Upwork Automation with n8n: How I Automate My Freelance Pipeline
I was spending 90 minutes a day checking Upwork manually. Now an n8n workflow does it every 15 minutes, scores every job with Claude, and drops a draft proposal in my Telegram before I've had my second coffee. Here's the exact setup.
The Problem: Manual Upwork Search Is Killing Your Win Rate
Here's something most freelancers don't talk about: response time is one of the biggest predictors of whether you get the job. Upwork's own data shows that clients open proposals in the order they arrive. If you're the 40th proposal on a good job, your chances drop dramatically — not because your profile is weak, but because the client found their person at proposal number 8.
I was losing the timing game every single day. My workflow was check Upwork, scroll through irrelevant listings, find a promising one, open the job, read the description, think about whether it fits my stack, then either skip it or spend 20 minutes writing a proposal. Repeat this 5-10 times a day across different searches and you've burned 90 minutes on admin before doing a single billable hour.
The win-rate math is brutal. Upwork jobs in the n8n/AI automation space get 10-30 proposals within the first two hours of posting. Within 15 minutes of posting, you're likely in the first 5. That 15-minute window is where client attention is highest and competition is lowest. No human can check Upwork every 15 minutes while also doing real work.
So I automated it. Not with some sketchy Upwork scraper that gets you banned, but with the RSS feed that Upwork exposes for every search result — a feature that's been sitting there since 2012 and almost nobody uses.
What the automation does:
- Monitors Upwork search results every 15 minutes via RSS feed
- Deduplicates so you never see the same job twice
- Scores each job 1-10 for fit using Claude API
- Filters to only surface jobs scoring 7 or higher
- Drafts a custom proposal using Claude with your profile context
- Notifies via Telegram with the job summary and draft proposal
- Logs everything to Airtable for win-rate tracking
The Architecture: Full Workflow Overview
Before we get into node-by-node setup, here's the full flow so you can see how the pieces connect:
RSS Feed Trigger (every 15 min) ↓ Deduplicate Items (by job ID) ↓ [For each new job] ↓ Extract Job Data (title, description, budget, client) ↓ Claude API — Score Job (1-10 fit score + reasoning) ↓ IF score >= 7 ↓ Claude API — Draft Proposal (personalized, ~200 words) ↓ Telegram Bot — Send notification with score + draft ↓ Airtable — Log job (id, score, title, budget, applied)
Total nodes: 8. Total setup time from scratch: about 2 hours if you follow this guide. Total time saved per week once running: 6-8 hours. I've been running this workflow for 90 days. Here's exactly how to build it.
One important note before we start: this workflow does not auto-submit proposals. I read the Telegram notification, review the draft, edit it if needed, then paste it manually into Upwork. The AI handles the boring part — finding and evaluating jobs, writing the first draft. The human part — final judgment and submission — stays with me. That's intentional and it's why this approach doesn't violate Upwork's ToS.
Step 1: Setting Up the Upwork RSS Feed Trigger
Upwork exposes an RSS feed for every search. This is the foundation of the whole workflow. Here's how to get your feed URL:
Go to Upwork, run a search for your target job type, then look at the URL. Add &format=rss to the end of any Upwork search URL and you get an RSS feed. For n8n automation jobs:
# Base Upwork search URL (run your own search and grab the URL) https://www.upwork.com/nx/search/jobs/?q=n8n+automation&sort=recency&format=rss # For multiple keywords, use separate feeds or combine searches: https://www.upwork.com/nx/search/jobs/?q=n8n+workflow+automation&sort=recency&format=rss https://www.upwork.com/nx/search/jobs/?q=claude+api+integration&sort=recency&format=rss # Always sort by recency — you want new jobs, not top-rated ones
In n8n, add an RSS Feed Read node. Configure it with your RSS URL and set the poll interval to 15 minutes. This is the sweet spot — frequent enough to catch jobs early, not so frequent that you hit rate limits.
// RSS Feed Read node config
{
"url": "https://www.upwork.com/nx/search/jobs/?q=n8n+automation&sort=recency&format=rss",
"pollInterval": 15, // minutes
"pollUnit": "minutes"
}
// Each item returned has these fields:
// - title: job title
// - link: URL to job posting
// - pubDate: when it was posted
// - content: full job description (HTML)
// - guid: unique job ID — use this for deduplicationRun multiple RSS triggers if you cover multiple niches. I run three: one for n8n/automation, one for Claude/AI integration work, and one for Hyros/attribution. Each feeds into the same scoring pipeline. Use n8n's Merge node to combine them before the deduplication step.
Deduplication
Without deduplication, you'll get notified about the same job every 15 minutes until it closes. Add an n8n Remove Duplicates node after the RSS trigger, keyed on the guid field. n8n stores seen GUIDs in memory — jobs you've already processed won't surface again in the same workflow execution cycle.
For persistence across workflow restarts, log processed job IDs to Airtable and do a lookup before scoring. I'll cover the Airtable logging setup in Step 6.
Step 2: Scoring Jobs with Claude API
This is the node that changed everything for me. Before automation, I'd spend 3-5 minutes reading a job description just to decide "not a fit." Claude does that in 2 seconds and gives me a score with reasoning.
Add an HTTP Request node after deduplication. Configure it to call the Claude API with this exact prompt structure:
// HTTP Request node — POST to Claude API
URL: https://api.anthropic.com/v1/messages
Method: POST
Headers:
x-api-key: {{ $env.ANTHROPIC_API_KEY }}
anthropic-version: 2023-06-01
Content-Type: application/json
Body (JSON):
{
"model": "claude-haiku-4-5-20251001",
"max_tokens": 300,
"system": "You are a job scoring assistant. Evaluate Upwork jobs for fit with a senior n8n automation and AI integration specialist. Return ONLY valid JSON.",
"messages": [
{
"role": "user",
"content": "Score this Upwork job 1-10 for fit with my profile:\n\nMY PROFILE:\n- 5+ years n8n automation expert\n- Claude/OpenAI API integrations\n- Hyros attribution specialist\n- Retell AI voice agents\n- Supabase, Airtable, webhooks\n- Based in Allen, TX (US timezone)\n\nJOB TITLE: {{ $json.title }}\n\nJOB DESCRIPTION:\n{{ $json.content }}\n\nReturn JSON only:\n{\"score\": 8, \"reason\": \"one-sentence explanation\", \"budget_ok\": true, \"timeline_ok\": true}"
}
]
}I use claude-haiku-4-5-20251001 for scoring, not Sonnet. Haiku is fast and cheap — scoring costs about $0.001 per job. I score 40-60 jobs per day across all my searches. That's less than $2/month in Claude API costs for the scoring step.
After the HTTP Request node, add a Code node to parse the response:
// Code node — parse Claude scoring response
const response = $input.first().json;
const content = response.content[0].text;
let scoreData;
try {
// Claude sometimes wraps JSON in markdown code blocks
const cleaned = content.replace(/```json\n?/g, '').replace(/```/g, '').trim();
scoreData = JSON.parse(cleaned);
} catch (e) {
// Fallback if parsing fails
scoreData = { score: 0, reason: 'Parse error', budget_ok: false, timeline_ok: false };
}
return [{
json: {
...items[0].json, // preserve original job data
score: scoreData.score,
score_reason: scoreData.reason,
budget_ok: scoreData.budget_ok,
timeline_ok: scoreData.timeline_ok
}
}];Next, add an IF node: score >= 7. Jobs scoring 6 and below get routed to the Airtable logging node (so you have data) but don't trigger a Telegram notification. You only hear about the good ones.
Scoring criteria Claude evaluates:
- Technical fit — Does the job require skills I have (n8n, Claude, Retell, Hyros)?
- Budget signals — Is the client's budget range realistic for the scope described?
- Client quality indicators — Payment verified, hiring rate, total spent mentioned in description?
- Scope clarity — Is the job well-defined enough to quote accurately?
- Red flags — "looking for cheap," "quick 5-minute task," unrealistic timelines
Step 3: Drafting Proposals with Claude
For jobs that score 7+, I run a second Claude call — this time using Sonnet for better writing quality — to draft a proposal. The key to making this work is injecting enough context about my background that the proposal doesn't sound like a template.
// HTTP Request node — Proposal drafting
{
"model": "claude-sonnet-4-6",
"max_tokens": 600,
"system": "You are a proposal writer for Carlos Aragon, a senior AI automation freelancer based in Allen, TX. Write concise, specific Upwork proposals that show technical understanding and don't use generic freelancer clichés. Never start with 'I am writing to express...' or 'I would love to work with you.' Always reference specific details from the job description to show you read it.",
"messages": [
{
"role": "user",
"content": "Write an Upwork proposal for this job. Keep it under 200 words. Be specific and direct.\n\nMY BACKGROUND:\n- 5+ years n8n automation workflows (200+ workflows built)\n- Claude API integrations — BellaBot (voice AI), content pipelines, lead scoring\n- Hyros attribution setup for 30+ ad accounts\n- Retell AI voice agents — $0.05/min optimized\n- Supabase, Airtable, webhooks, REST APIs\n- US timezone (Allen, TX), async-friendly\n- Recent wins: 60% Claude API cost reduction, unified lead database for multi-channel client\n\nJOB TITLE: {{ $json.title }}\n\nJOB DESCRIPTION:\n{{ $json.content }}\n\nSCORE REASON: {{ $json.score_reason }}\n\nWrite the proposal now. End with a specific question about their setup or timeline."
}
]
}The final line of the prompt — "end with a specific question" — is important. Proposals that ask a relevant question about the client's setup show you actually read the job and open a conversation. My response rate went from 18% to 34% after I added that instruction.
After this node, add another Code node to extract the proposal text from the API response:
// Extract proposal text from Claude response
const response = $input.first().json;
const proposalText = response.content[0].text;
return [{
json: {
...items[0].json,
proposal_draft: proposalText
}
}];Step 4: Telegram Notifications That Actually Work
The notification is where the workflow pays off in real time. A well-formatted Telegram message means you can evaluate a job and copy a draft proposal in under 2 minutes, from your phone, while the job is still fresh.
Add a Telegram node and configure it with your bot token and chat ID. The message template matters — pack everything useful into one message without making it unreadable:
// Telegram node — message text (use Markdown parse mode)
🎯 *New Upwork Match — Score: {{ $json.score }}/10*
*{{ $json.title }}*
📊 *Fit Analysis:* {{ $json.score_reason }}
🔗 {{ $json.link }}
---
✍️ *Draft Proposal:*
{{ $json.proposal_draft }}
---
_Posted: {{ $json.pubDate }}_Set the Telegram node to parse mode Markdown. The message structure puts the score at the top (so you know immediately if it's worth reading), the link for quick access, and the draft at the bottom for easy copying.
My workflow to apply is now: read Telegram notification → open job link → edit the draft proposal to add any specific details I want → paste into Upwork → submit. Total time: 4-7 minutes per application vs. the previous 20-25 minutes.
Step 5: Logging to Airtable for Win Rate Tracking
Every job that passes through the workflow gets logged to Airtable — scored or not, applied or not. This data is invaluable after 30 days. You'll start seeing patterns: which job categories score high but convert poorly, what budget ranges work for you, which clients ghost after interview.
// Airtable node — Create Record
Table: upwork_jobs
Fields mapping:
job_id → {{ $json.guid }}
title → {{ $json.title }}
url → {{ $json.link }}
posted_at → {{ $json.pubDate }}
score → {{ $json.score }}
score_reason → {{ $json.score_reason }}
applied → false (update manually when you apply)
hired → false (update manually when you win)
notified → {{ $json.score >= 7 ? true : false }}
detected_at → {{ $now }}The applied and hired fields you update manually — either directly in Airtable or via a simple Telegram bot command. After 90 days I have a clean dataset: 1,847 jobs detected, 312 scored 7+, 89 applied, 14 contracts won.
That 15.7% win rate from applied jobs is more than double my pre-automation rate of 7%. The improvement isn't just from speed — it's from only applying to jobs that are genuinely good fits (score 7+) instead of spraying proposals to see what sticks.
Airtable views I check weekly:
- Applied but not hired — What did these jobs have in common? Was my score calibration off?
- Score 8-10 but didn't apply — Did I miss good opportunities? Should I widen my search terms?
- Win rate by score bucket — Do score-7 jobs convert at the same rate as score-9 jobs? (They don't.)
- Average time from posted to applied — Am I still hitting the early window?
The Full n8n Workflow JSON
Here's an abbreviated version of the workflow JSON you can import directly into n8n. Replace the placeholder values with your own API keys and IDs:
{
"name": "Upwork Job Monitor",
"nodes": [
{
"name": "RSS Trigger",
"type": "n8n-nodes-base.rssFeedReadTrigger",
"parameters": {
"url": "https://www.upwork.com/nx/search/jobs/?q=n8n+automation&sort=recency&format=rss",
"pollTimes": { "item": [{ "mode": "everyX", "value": 15, "unit": "minutes" }] }
}
},
{
"name": "Remove Duplicates",
"type": "n8n-nodes-base.removeDuplicates",
"parameters": {
"fieldToCompare": "guid"
}
},
{
"name": "Score with Claude",
"type": "n8n-nodes-base.httpRequest",
"parameters": {
"url": "https://api.anthropic.com/v1/messages",
"method": "POST",
"headers": {
"x-api-key": "={{ $env.ANTHROPIC_API_KEY }}",
"anthropic-version": "2023-06-01"
},
"body": {
"model": "claude-haiku-4-5-20251001",
"max_tokens": 300,
"messages": [{ "role": "user", "content": "..." }]
}
}
},
{
"name": "Parse Score",
"type": "n8n-nodes-base.code",
"parameters": { "jsCode": "// parse Claude response..." }
},
{
"name": "Score Filter",
"type": "n8n-nodes-base.if",
"parameters": {
"conditions": {
"number": [{ "value1": "={{ $json.score }}", "operation": "gte", "value2": 7 }]
}
}
},
{
"name": "Draft Proposal",
"type": "n8n-nodes-base.httpRequest",
"parameters": {
"url": "https://api.anthropic.com/v1/messages",
"body": { "model": "claude-sonnet-4-6", "max_tokens": 600 }
}
},
{
"name": "Send Telegram",
"type": "n8n-nodes-base.telegram",
"parameters": {
"chatId": "={{ $env.TELEGRAM_CHAT_ID }}",
"text": "🎯 New Match — Score: {{ $json.score }}/10..."
}
},
{
"name": "Log to Airtable",
"type": "n8n-nodes-base.airtable",
"parameters": { "operation": "create", "table": "upwork_jobs" }
}
]
}To import: open n8n, go to Workflows, click the menu, select "Import from JSON." You'll need to reconnect credentials (Telegram bot, Airtable API key, Anthropic API key) after importing.
90 Days Later: Real Results
I activated this workflow on January 3, 2026. Here's the actual data from 90 days of running it:
The 15.7% close rate from applied jobs vs. my pre-automation 7% is the headline number, but it understates the real value. What I don't have in that table: the 6-8 hours per week I got back. The jobs I now catch within 10 minutes of posting that I would have missed at my old check frequency. The proposals I submit that feel tailored instead of copy-pasted.
Total Claude API cost for 90 days: $47. Haiku for 1,847 scoring calls, Sonnet for 312 proposal drafts. Against the value of 14 contracts and 6 hours of weekly time saved, it's not even worth calculating the ROI.
What I'd Do Differently
Start with one search, not three. I launched with three RSS feeds simultaneously and immediately had deduplication issues when the same job appeared in multiple searches. Get one feed working perfectly, then add more.
The score threshold should be 7, not 8. I started at 8 and missed opportunities. Score-7 jobs where the fit reason mentions "related experience" often convert well. Now I get all 7+ in Telegram and apply my own filter at review time.
Log the proposal draft, not just the job. My original Airtable setup didn't save the proposal Claude generated. When I wanted to analyze what proposal styles won vs. lost, I had no data. Now I log proposal_draft in Airtable and track whether I edited it significantly before submitting.
Add a "client history" prompt upgrade. The current workflow can't see the client's hire rate or payment history because RSS feeds don't include that data. If a job's link includes the client's profile ID, a second API call could fetch that data from Upwork's public API and factor it into the score. That's the next upgrade I'm building.
Build It This Week
The freelance marketplace is a timing game. The best proposals win not just because they're well-written — they win because they arrive first, they're specific, and they show the client that the freelancer actually read their requirements. Automation handles the first two. Claude handles the third.
You don't need to be an automation expert to build this. If you've used n8n before and have an Anthropic API key, the core workflow is 2 hours of setup. If this is your first n8n workflow, budget 4 hours and follow the steps above sequentially. The RSS trigger is the hardest part to troubleshoot — make sure &format=rss is appended to your search URL and test it in a browser before adding the n8n node.
One month from now you'll wonder how you ever found jobs manually.
Want me to build this for your Upwork profile?
I set up custom n8n automation pipelines for freelancers and agencies. The Upwork workflow, tuned for your niche with your profile context, typically takes 3-4 hours. If you're interested in having a customized version built, reach out.
Get in touch →