it('renders this post', () => {
October 8, 2026 11 min read

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.

StepUpworkClaudeMe
Find jobsHolds the postings and searchRuns six searches on a scheduleChooses the search phrases
Read and filterShows budget, proposals, client history, screening questionsReads every detail and applies my filtersWrites the filters
DecideShows the client’s recordScores the job from 0 to 100Sets the threshold
ApplyReceives the proposalDrafts it and submits only inside my safety rulesApproved the rules in advance
Tell meShows replies and invitationsSends a phone notification only when it mattersMakes 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.

  1. Connect Upwork to Claude and enable scheduled tasks.
  2. List your real background and what you lack.
  3. Write approved answers to common client questions.
  4. Set searches, a freshness window and filters.
  5. Score jobs 0-100 and pick a threshold (I use 70).
  6. Add safety rules: caps, no invented claims, stop when unsure.
  7. Schedule it, drafts only at first.
  8. 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.

PlanScheduled tasks
FreeNot available
ProAvailable
MaxAvailable
Team and EnterpriseAvailable (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.

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

← Back to all posts