Automated My Upwork Job Hunt with Claude
A scheduled Claude task searches Upwork every 6 hours, filters and scores jobs, and submits only strong, safe matches. Below: how it works, how to build it, and a demo script.
The problem
Good Upwork jobs fill with proposals within hours, and sorting them by hand takes all day. Blind auto-apply tools overclaim. I wanted help that stays honest.
What it does, every six hours
A scheduled Claude task runs six searches, filters, scores, then submits or notifies me.

Every run follows the same path. Only a strong job that passes every safety rule is submitted.
How Upwork and Claude work together
Claude reads and acts on Upwork through a connection to your account.
| Step | Upwork | Claude | Me |
|---|---|---|---|
| Find jobs | Holds the postings and search | Runs six searches on a schedule | Chooses the search phrases |
| Read and filter | Shows budget, proposals, client history, screening questions | Reads every detail and applies my filters | Writes the filters |
| Decide | Shows the client’s record | Scores the job from 0 to 100 | Sets the threshold |
| Apply | Receives the proposal | Drafts it and submits only inside my safety rules | Approved the rules in advance |
| Tell me | Shows replies and invitations | Sends a phone notification only when it matters | Makes the judgment calls |
How it is helping so far
Early days. No job wins claimed yet.
- No more scrolling listings; sorting happens while I sleep.
- I only see decisions, never noise.
- It flags jobs I would rush past, and never bluffs.
The part I care about most: what it must never do
- Never contacts clients, accepts offers or edits my profile.
- Hard caps: 3 proposals per run, a connects floor, a cost limit per job.
- Only uses my real background and approved answers. No invented claims.
- Unanswerable screening question? It sends me the draft instead.
- Verified clients only.
- Anything odd: stop and report.
Filters that save everyone’s time
- More than 10 proposals already.
- Any country, language or fixed-hours restriction I can’t meet. US Eastern hours are fine; “US only” is a skip.
- Skills or tools I lack.
- Unverified client, no spend, small budget.
- Shady work.
Low budgets are checked first: a negotiable price gets a draft for me, not an auto-bid.
Treating an AI agent like software under test
One run submitted nothing, correctly. It flagged a job needing device models I lack, a Magento role needing experience I lack, and a client with unverified payment. I then added the country and language check. Run, read, add a rule, repeat.
How to build your own, step by step
No code needed: one prompt plus an Upwork connection, on a schedule.

- Connect Upwork to Claude and enable scheduled tasks.
- List your real background and what you lack.
- Write approved answers to common client questions.
- Set searches, a freshness window and filters.
- Score jobs 0-100 and pick a threshold (I use 70).
- Add safety rules: caps, no invented claims, stop when unsure.
- Schedule it, drafts only at first.
- Read every run, then add a rule for each surprise.
Check Upwork’s current terms on automation, and keep a human reviewing.
Which Claude plan do you need?
According to Claude’s help center, scheduled tasks are available on all paid plans: Pro, Max, Team and Enterprise. The Free plan is not listed,, so on the Free plan you should not expect to see the scheduler.
| Plan | Scheduled tasks |
|---|---|
| Free | Not available |
| Pro | Available |
| Max | Available |
| Team and Enterprise | Available (admins can switch Cowork access on or off) |
Tasks run remotely, even if your computer is asleep. Usage impact isn’t documented, so start modest.
Plans and features change, so check the scheduled tasks help article before you subscribe.
A prompt skeleton you can copy
Replace the angle brackets with your details.
You are my Upwork job scout. Every run: find fresh jobs, score them,
and <DRAFT ONLY | submit proposals for jobs that meet the rules below>.
Never message clients, accept or decline offers, boost proposals,
edit my profile or withdraw proposals.
SETUP
- Read my profile once and ground every claim in it.
- Background: <role, years, skills, certifications, tools, timezone,
weekly availability, hourly rate>.
- I do NOT have: <skills, tools, certifications, languages, domains>.
APPROVED ANSWER BANK (do not invent beyond these)
- <common client question>: <true answer>
- <common client question>: <true answer>
If a screening question is not covered, do not submit. Show me the draft.
SEARCH
- Run one search per phrase: <phrase 1>, <phrase 2>, <phrase 3>.
- Keep only jobs posted in the last <12> hours. Skip jobs I applied to.
FILTER (drop silently)
- More than <10> proposals already.
- Any country, region, language or fixed-hours requirement I cannot meet.
- Needs something from my "I do NOT have" list.
- Unverified payment with no spend history and a small budget.
- Budget below <minimum>, unless it looks negotiable.
SCORE 0-100
- Skill fit <30>, budget <20>, client quality <20>,
scope clarity <15>, competition <10>, long-term potential <5>.
- <70>+ and passes the safety rules: act. 50-69: one-line note.
Below 50: skip.
SAFETY RULES (all must hold, or do not submit)
- At most <3> proposals per run.
- Never go below <40> connects. Never pay more than <20> connects per job.
- No placeholders, no invented results, clients, samples or certificates.
- Client payment must be verified.
- If anything looks wrong, stop and report. Do not retry blindly.
PROPOSAL STYLE
- Open with the client's problem. Short numbered approach.
Say what I would not cover. End with one or two useful questions.
REPORT
- If nothing was submitted and nothing scored 50+, say "No new matches"
and send no notification.
- Otherwise send a short notification: what was submitted, what needs
my decision, and the borderline list.
Start with DRAFT ONLY for the first few days. Once the drafts read the way you would write them, switch to submitting.
No setup? Try the filtering with a demo script
Can’t set up an agent? This Python 3 script filters and scores jobs with the same logic. It never applies.
"""Demo job scout: filter + score job postings, draft-only (never submits).
Run: python3 job_scout.py (uses the sample jobs below)
python3 job_scout.py jobs.json (your own export: a list of dicts with the same keys)
"""
import json, re, sys
# ---- 1. YOUR SETTINGS: edit these -------------------------------------------
MY_SKILLS = {"qa", "test automation", "api testing", "cypress", "selenium",
"postman", "manual testing", "technical writing", "youtube"}
MISSING = {"playwright", "k6", "jmeter", "supabase", "oscp", "coupa", "magento"}
OK_TIERS = {"Fewer than 5", "5 to 10"} # competition you accept
MIN_HOURLY_MAX, MIN_FIXED = 12, 50 # budget floors (USD)
MY_RATE = 15 # target hourly rate
THRESHOLD, BORDERLINE = 70, 50
# Wording that restricts applicants by place, language or hours.
# Tip: this list is deliberately strict. If unsure, skip.
LOCATION_RE = re.compile(
r"\b(us|usa|u\.s\.)[\s-]*(only|based|local|residents?)\b"
r"|\b(must|should) (be )?(located|based|reside|live)\b"
r"|\b(residents?|citizens?) of\b|\blocal only\b|\bwork authori[sz]ation\b"
r"|\bnative [a-z]+ speakers?\b|\bnative speakers?\b"
r"|\b(uk|cet|pst|gmt)\b[^.]{0,20}\b(hours|time ?zone|only)\b"
r"|\bon-?site\b|\bin[- ]person\b|\bno [a-z]+ applicants\b"
r"|\b(philippines|latam|latin america|europe|eu|canada|australia)[\s-]*(only|based)\b",
re.I)
SHADY = ("fake review", "buy followers", "bot views", "gambling", "adult", "black hat")
# ---- 2. FILTERS: each returns a reason to skip, or None ------------------------
def skip_reason(j):
text = " ".join([j["title"], j["description"], " ".join(j.get("questions", []))]).lower()
if j.get("applied"):
return "already applied"
if j["proposals_tier"] not in OK_TIERS:
return "too much competition"
if j.get("preferred_countries") or j.get("location_required"):
return "country restriction (structured field)"
if LOCATION_RE.search(text):
return "location/language/hours restriction in text"
if any(m in text for m in MISSING):
return "needs a skill or tool I lack"
if any(s in text for s in SHADY):
return "shady work"
c = j["client"]
if not c["payment_verified"] and c["spent"] == 0 and (j.get("budget_max") or 0) < MIN_FIXED:
return "unverified client, no spend, small budget"
low = j["budget_max"] < (MIN_HOURLY_MAX if j["type"] == "hourly" else MIN_FIXED)
negotiable = re.search(r"negotiable|open budget|can be discussed", text) or c["spent"] >= 10000
if low and not negotiable:
return "budget too low"
return None
# ---- 3. SCORE (weights add up to 100) ------------------------------------------
def score(j):
text = (j["title"] + " " + j["description"]).lower()
hits = sum(1 for s in MY_SKILLS if s in text)
tech = min(30, hits * 10)
top = j["budget_max"]
budget = 20 if (j["type"] == "hourly" and top >= MY_RATE) or (j["type"] == "fixed" and top >= 100) \
else round(20 * min(top / (MY_RATE if j["type"] == "hourly" else 100), 1))
c = j["client"]
client = (8 if c["payment_verified"] else 0) + min(6, c["spent"] // 1000) + (6 if c["rating"] >= 4.5 else 2)
scope = 15 if len(j["description"]) > 200 else 7
comp = 10 if j["proposals_tier"] == "Fewer than 5" else 6
longterm = 5 if j.get("long_term") else 2
return tech + budget + client + scope + comp + longterm
# ---- 4. RUN --------------------------------------------------------------------
def main(jobs):
for j in jobs:
why = skip_reason(j)
if why:
print(f"SKIP {j['title'][:48]:48} -> {why}")
continue
s = score(j)
tag = "DRAFT" if s >= THRESHOLD else "MAYBE" if s >= BORDERLINE else "LOW "
print(f"{tag} {j['title'][:48]:48} score {s}")
print("\nDraft-only: review DRAFT jobs and apply by hand (or wire in your own submit step).")
SAMPLE = [
dict(title="QA automation engineer for fintech app", type="hourly", budget_max=25,
proposals_tier="Fewer than 5", description="We need Cypress and API testing with Postman for a payments platform. " * 3,
client=dict(payment_verified=True, spent=8000, rating=4.9), long_term=True),
dict(title="QA tester (US only)", type="hourly", budget_max=30, proposals_tier="5 to 10",
description="Manual testing. Candidates must be located in the United States. " * 3,
client=dict(payment_verified=True, spent=500, rating=5)),
dict(title="Social media manager - native Romanian speaker", type="hourly", budget_max=25,
proposals_tier="Fewer than 5", description="Grow our brand on social media. " * 6,
client=dict(payment_verified=True, spent=0, rating=0)),
dict(title="Playwright SDET", type="hourly", budget_max=40, proposals_tier="Fewer than 5",
description="Playwright and k6 load testing. " * 6, client=dict(payment_verified=True, spent=3000, rating=4.8)),
dict(title="API testing quick job", type="fixed", budget_max=20, proposals_tier="5 to 10",
description="Test a REST API with Postman. " * 8, client=dict(payment_verified=True, spent=200, rating=4.8)),
dict(title="YouTube channel growth", type="hourly", budget_max=20, proposals_tier="10 to 15",
description="Grow our YouTube channel. " * 8, client=dict(payment_verified=True, spent=4000, rating=4.9)),
dict(title="Technical writer for API docs", type="hourly", budget_max=18, proposals_tier="5 to 10",
description="Technical writing for REST API documentation, US Eastern hours check-ins are fine. " * 3,
client=dict(payment_verified=True, spent=1500, rating=4.7)),
dict(title="Unverified newcomer: Postman API testing", type="hourly", budget_max=18,
proposals_tier="Fewer than 5", description="Postman API testing. " * 10,
client=dict(payment_verified=False, spent=0, rating=0)),
]
if __name__ == "__main__":
data = json.load(open(sys.argv[1])) if len(sys.argv) > 1 else SAMPLE
for j in data:
j.setdefault("questions", [])
main(data)
Running it on the built-in sample jobs prints this:
DRAFT QA automation engineer for fintech app score 100
SKIP QA tester (US only) -> location/language/hours restriction in text
SKIP Social media manager - native Romanian speaker -> location/language/hours restriction in text
SKIP Playwright SDET -> needs a skill or tool I lack
SKIP API testing quick job -> budget too low
SKIP YouTube channel growth -> too much competition
MAYBE Technical writer for API docs score 68
SKIP Unverified newcomer: Postman API testing -> unverified client, no spend, small budget
For real jobs, save them in jobs.json with the same fields and run python3 job_scout.py jobs.json. Its text matching is simple, so review what passes.
Related resources
These are the official pages I used or would start with. Read each one for the latest details, because features and plans change.
- Schedule recurring tasks in Claude Cowork: how scheduled tasks work, which plans include them, and where they run.
- Get started with Claude Cowork: what Cowork is, and which plans and devices it supports.
- Prompt engineering overview: how to write clear, detailed instructions, which is the skill behind the prompt skeleton above.
- Upwork developer documentation: for readers who want to write their own script against Upwork’s GraphQL API. It lists the available APIs (job posts, proposals, contracts and search) and says you need API keys and an account that meets their requirements. Read their terms of use first.
- Upwork API reference: the GraphQL reference for the data you could feed into the demo script.
What this says about how I work
Senior QA engineer in Dhaka (UTC+6). US Eastern hours fine, 20 hours a week, can start now.
Need QA, API testing or YouTube/Facebook growth? Message me on Upwork.
My Upwork Profile – https://www.upwork.com/freelancers/~014206f6e866954a66
