9b395f5cc3
E2E Headed Chrome / e2e-headed (macos-15) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (ubuntu-latest) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (windows-latest) (push) Has been cancelled
CI / build (macos-latest) (push) Has been cancelled
CI / build (ubuntu-latest) (push) Has been cancelled
CI / build (windows-latest) (push) Has been cancelled
CI / unit-test (push) Has been cancelled
CI / bun-test (push) Has been cancelled
CI / adapter-test (push) Has been cancelled
CI / smoke-test (macos-latest) (push) Has been cancelled
CI / smoke-test (ubuntu-latest) (push) Has been cancelled
Security Audit / audit (push) Has been cancelled
Build Chrome Extension / build (push) Has been cancelled
Trigger Website Rebuild (Docs Updated) / dispatch (push) Has been cancelled
5.8 KiB
5.8 KiB
Upwork
Mode: 🔐 Browser · Domain: upwork.com
Commands
| Command | Description |
|---|---|
opencli upwork search <query> |
Upwork keyword job search (logged-in browser session, US site) |
opencli upwork feed [tab] |
Personalized jobs feed — best-matches (default) or most-recent |
opencli upwork detail <id> |
Read the full Upwork job posting by ciphertext id |
Usage Examples
# Search jobs by keyword (default 10 rows, sort by recency)
opencli upwork search "python"
# Filter and paginate
opencli upwork search "react developer" --location "United States" --sort relevance --page 2 --per_page 25
# Personalized recommended feed (requires login)
opencli upwork feed --limit 20
# Switch to the chronological feed
opencli upwork feed most-recent --limit 10
# Full job posting (id is the ciphertext form from `search` / `feed`)
opencli upwork detail "~022055006392174412621"
# Detail also accepts the full /jobs/ URL
opencli upwork detail "https://www.upwork.com/jobs/~022055006392174412621"
# JSON output
opencli upwork search "python" -f json
opencli upwork detail "~022055006392174412621" -f json
Output
search and feed
Both commands return the same column set so feeds and search results can be compared / unioned:
| Column | Type | Notes |
|---|---|---|
rank |
number | 1-based row index. For search it is the global rank across pages ((page-1) * per_page + i). |
id |
string | Ciphertext form (~01… / ~02…). The stable public id for cross-page lookups. |
title |
string | Job title with Upwork's <span class="highlight"> query markup stripped. |
type |
string | hourly or fixed (decoded from the numeric type field). |
budget |
string | Human-readable: $40-$70/hr for hourly ranges, $30/hr for single bounds, $200 for fixed-price, '' when client didn't set one. |
experienceLevel |
string | entry, intermediate, expert, or ''. Decoded from tierText (search) / tier (feed). |
proposalsTier |
string | Compact bucket: <5, 5-10, 10-15, 20-50, 50+. Search uses the i18n-keyed form, feed uses the rendered label — both normalize to the same output. |
skills |
string | Comma-separated skill names from attrs[] / skills[]. Deduped. |
clientCountry |
string | Country name (United States) or ISO code (BGD) — Upwork is inconsistent across rows. |
clientRating |
number | null | Average feedback score 0-5. null when the client has no reviews (we deliberately don't surface 0 as a real score). |
publishedOn |
string | ISO 8601 timestamp from publishedOn (falls back to createdOn). |
url |
string | Absolute /jobs/<ciphertext> URL. |
detail
| Column | Type | Notes |
|---|---|---|
id |
string | Ciphertext form, normalized from the input arg. |
title |
string | Full title, highlight markup stripped. |
type |
string | hourly or fixed. |
budget |
string | Same format as search, but read from extendedBudgetInfo + budget.amount. |
experienceLevel |
string | entry / intermediate / expert — decoded from the numeric contractorTier (1 / 2 / 3). |
workload |
string | Pre-rendered workload string (More than 30 hrs/week etc.) or ''. |
category |
string | Top-level category name (Web Development). |
skills |
string | Same shape as list rows. |
description |
string | Full job description body. |
clientCountry |
string | From buyer.location.country. |
clientSpent |
number | null | Lifetime client spend in USD (buyer.stats.totalCharges.amount). null when zero / missing. |
clientHires |
number | null | Number of past hires (buyer.stats.totalJobsWithHires). |
clientRating |
number | null | Same null-on-zero rule as the list commands. |
proposalsCount |
number | null | Real proposals count from clientActivity.totalApplicants — more precise than the bucket on list rows. |
publishedOn |
string | ISO 8601 from publishTime (falls back to postedOn / createdOn). |
url |
string | Absolute /jobs/<ciphertext> URL. |
Prerequisites
- Chrome running and logged into upwork.com
- Browser Bridge extension installed
- The connected Chrome profile needs to actually own the Upwork session for
feed(best-matches/most-recentredirect to onboarding for visitors)
Caveats
- Read-only. No commands write to your Upwork account. There is no
apply/submit-proposal/withdrawcommand — proposing for jobs is deliberately out of scope. - No proposals command. Listing your own submitted proposals is intentionally not shipped. Upwork's
listsVuex module is the right source, but verifying the field shape end-to-end requires an account with real proposals — once that data is available, see the field-map notes in~/.opencli/sites/upwork/. - Cloudflare sits in front of every surface — all commands run through your logged-in browser session (
Strategy.COOKIE,browser: true). Barefetchreturns a__cf_bmchallenge. If the adapter sees the challenge page it raisesCommandExecutionErrorwith a hint to clear it in the connected browser. - List data comes from SSR state, not DOM scraping. Upwork's card class names change often; instead the adapter reads
window.__NUXT__.state.{jobsSearch,feedBestMatch,feedMostRecent}.jobs[]directly. Detail reads from the Vuex store atwindow.$nuxt.$store.state.jobDetails.{job,buyer}. This is more durable but means UI freshness / DOM tweaks have no effect — what you see in the browser may briefly differ from what the adapter returns if Upwork re-hydrates mid-load. - Login redirect raises
AuthRequiredError(exit 77), not an empty result. per_pageis clamped to the [10, 50] range that Upwork's search will honor.--limitonfeedis [1, 50].