Skip to content
Automation
Skill

/upwork-search

Search Upwork through the official Upwork MCP, smart-filter out low-quality clients, rank survivors by fit, and save them to the campaign as recommendations. Never submits a proposal.

From plugin
jobpilot
6931 skills2 agents2 MCP
Install
$ npx -y skills add suxrobGM/jobpilot --skill upwork-search --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/upwork-search

Context preview

The summary Claude sees to decide when to auto-load this skill.

Search Upwork through the official Upwork MCP, smart-filter out low-quality clients, rank survivors by fit, and save them to the campaign as recommendations. Never submits a proposal.

SKILL.md

upwork-search.SKILL.md
name: upwork-search
description: Search Upwork through the official Upwork MCP, smart-filter out low-quality clients, rank survivors by fit, and save them to the campaign as recommendations. Never submits a proposal.
argument-hint: "<job_keywords> --board upwork.com [--max-jobs N] [--campaign <campaign-id>]"

Upwork Job Search & Recommend

Find Upwork jobs the user can win - qualify on fit **and** client quality, drop the junk, and save the keepers to the campaign for review. **Recommend only: never submit a proposal here.** The user drafts a proposal (the `upwork-proposal` skill, launched per job from the campaign page) and sends it with the `upwork-submit` skill.

Setup

1. Follow `../_shared/setup.md` (`$JOBPILOT_API` is injected by the terminal). 2. Follow `../_shared/upwork-mcp.md`: confirm the Upwork tools are connected and resolve `ORG_UID` once. Not connected → stop with the message that doc gives. There is no browser fallback. 3. Parse and strip the flags; the rest is the free-text query.

  • `--board upwork.com` - required.
  • `--max-jobs <N>` - cap on results to evaluate. Absent = 30. Pages are 10 at most, so the cap

sets how many cursor round trips you make.

  • `--campaign <id>` - campaign to save to. The UI passes it; if absent, match the latest

`source:"search"`, `status:"in_progress"` campaign on the query, else create one (a `source:"search"` create requires `config.resumeId` - default to the profile's `primaryResumeId`). 4. Resolve the board: `curl -fsS -H "authorization: Bearer $JOBPILOT_API_TOKEN" "$JOBPILOT_API/api/job-boards" | jq '.[] | select(.domain=="upwork.com")'`. No row → abort: "Upwork is not configured. Add it on /boards." If a `--campaign` was given, first command it to `failed` with `POST /api/campaigns/<id>/status {"status":"failed"}`.

Phase 1: Parse Query

Extract the role and skills, and the preferences the search can filter on directly: hourly vs fixed, budget or rate floor, client location, weekly hours, project length. If vague, ask before searching.

Push every preference you extracted into the search parameters rather than filtering afterwards. Always set `verified_payment_only: true` and `proposals_max: 49` - both are hard skips downstream, so paying for those rows wastes a page.

Phase 2: Search Upwork

Two sources, merged into one pool and deduped on job id.

1. **Recommendations** - `find_jobs` action `smart_search`. This is Upwork's own recommender and already knows the profile, so pass no query and no skills, only the hard filters from Phase 1. Use `mode: "best_match"`. Add `days_posted` when the user asked for fresh work. 2. **Keyword search** - `find_jobs` action `search`. Use `title` (1-3 words) when the user named a role, since every returned job then carries those words in its title. Use `query` only for a broad topic. They cannot be combined, and `sort: "relevance"` silently drops `title`, `skills` and `category`, so leave `sort` unset.

Split `--max-jobs` across the two sources. Page with the cursor only while `hasNextPage` or `hasMore` is true, repeating the same filters each time.

Location: pass `location` (a country or region name, e.g. `"United States"`) when the user wants it. A value outside Upwork's vocabulary returns an empty page rather than an error, so on zero results read `empty_result_note` and retry with the spelling it names.

Drop any row whose `applied` or `is_applied` is true - the user already applied through Upwork.

Phase 3: Evaluate Each Result

3.1 Dedupe

Run the applied-check (`../_shared/campaign-flow.md`) with the client name as the company. `.applied` → record the default already-applied skip, then skip the rest.

3.2 Client quality (smart filter)

Score the client from the **search row alone**, before opening the posting. Every hard rule reads a row field, so a client that fails one costs a single call instead of a full job description pulled into context.

CLIENT='{ "paymentVerified": true, "totalSpent": 12000, "rating": 4.9,
  "reviewsCount": 24, "proposalsCount": 7, "postedHoursAgo": 6, "jobType": "hourly" }'
QUALITY=$(curl -fsS -H "authorization: Bearer $JOBPILOT_API_TOKEN" -X POST "$JOBPILOT_API/api/upwork/client-quality" \
  -H 'content-type: application/json' -d "$(jq -n --argjson c "$CLIENT" '{client:$c}')")
CLIENT_VERDICT=$(echo "$QUALITY" | jq -r '.verdict')   # good | caution | skip

Field sources: `paymentVerified` from `verification_status`, `totalSpent` from `total_spent`, `reviewsCount` from `total_reviews`, `proposalsCount` from `proposal_count`, `postedHoursAgo` computed from `created_date` or `published_date`. Omit any you cannot read - every field is optional and a missing one degrades to neutral. `rating` is the score **freelancers gave this client**, so a low one is a warning about the client, not a sign they are unsuccessful.

The scorer hard-skips unverified payment, 50+ proposals, and unproven-plus-unverified clients. If `CLIENT_VERDICT == "skip"`, create the Job as `pending`, record `.skipReason` through `/jobs/<key>/result`, and move on - don't open the posting and don't score fit.

3.3 Open the posting and rescore

Survivors only. Call `find_jobs` action `get` with the row's numeric `id` for three things: the full description, `client_record` (the hire count the search row cannot show), and `connects_cost`, which the proposal step needs later. Keep only the fields the digest and the client block use - do not carry the whole payload forward.

Add `clientHires` from `client_record` to `CLIENT` and score once more. The hire count is 20% of the score, so this second verdict is the one to trust and its `qualityScore` the one to save. A `skip` here is the soft floor rather than a hard rule; handle it exactly as above.

3.4 Fit

Build the digest (`../_shared/digest-schema.md`) from the posting you already fetched; always populate `skills`. Score inline - the MCP returns the full descri

Read more
Ships withjobpilot

An AI agent that applies to jobs for you, on the Claude or Codex subscription you already have.

Get the whole plugin

Other skills on jobpilot.