Find relevant work. Keep a history of what you have seen. Write proposals from your own evidence.
Built for the Early AI-dopters community, this CLI brings Upwork search, lane-based ranking, SQLite history, and proposal preparation into one terminal workflow. A dedicated Chrome profile handles your signed-in session. Your evidence profile supplies your experience and voice.
Open source, MIT licensed, and not affiliated with Upwork. Everything runs on your machine against your own signed-in account. Offline tests and a clean package installation are verified on every change. Browser automation depends on Upwork’s current pages and can stop when those pages change; fixes are welcome as pull requests. Filling and submitting proposals through the browser is off by default; see Live proposal automation.
Get started · Pick your lanes · Find work · Write like yourself · Proposal workflow · Troubleshooting
| Capability | What it does |
|---|---|
| Lanes | Pick the kinds of work you want; each lane brings its own searches and ranking signals |
| Search and hunts | Search one phrase or run every query in your lanes with duplicate jobs removed |
| Net-new results | Save every observation, then show jobs first seen in the current pull |
| Local ranking | Explain the lane, keyword, budget, activity, and client signals behind a score |
| Eligibility inspection | Open job details to check location restrictions omitted from search cards |
| Persistent history | Keep jobs, sightings, and pull runs in local SQLite |
| Evidence packets | Pair exact screening questions with your relevant proof |
| Human writing guides | Build your voice profile, draft from evidence, and edit specific weak patterns |
| Guarded proposals | Review and paste yourself, or opt in to guarded browser filling and submission |
| Portable exports | Export tables, JSON, JSONL, CSV, or Markdown |
The CLI does not generate prose by calling an AI model. Use the packet with your preferred assistant or write the application yourself. No model API key is needed.
You need Node.js 22.5 or later, npm, Google Chrome, and an Upwork freelancer account. Chrome discovery includes macOS, Windows, and Linux paths; live behavior must be checked on your platform. The CLI uses Node’s built-in SQLite, which may print an experimental-feature warning on older supported Node versions.
Clone and install:
git clone https://github.com/earlyaidopters/upwork-cli.git
cd upwork-cli
npm ci
npm link
upwork-cli setup
upwork-cli auth
upwork-cli doctornpm link installs the local command. You can skip it and run node src/cli.mjs instead of upwork-cli.
auth opens a dedicated Chrome window. Sign in there and complete any verification yourself. The CLI attaches to that session through localhost. It never needs cookies copied from your everyday browser.
init creates private configuration and profile files. It leaves existing files intact. init --force overwrites both files, so use it only when you intend to reset them.
setup asks for your country, hourly rate, and lanes, saves them privately, and preserves your existing evidence and preferences. Press Enter to keep a field unchanged. It does not open a browser or apply to jobs. For an assistant or a non-interactive terminal, supply explicit values:
upwork-cli setup --country "Canada" --rate 100 --lanes ai-automation,ai-agentsThese are examples; use your own country, rate, and lanes. upwork-cli init remains available for empty defaults.
Edit ~/.upwork-cli/config.json. Set eligibility.freelancerCountry to your actual country; the default is null. For example:
{
"eligibility": {
"freelancerCountry": "Canada",
"inspectTop": 25,
"excludeIneligible": true
}
}Canada is an example, not an account setting. Keep the other generated fields or supply a partial config; defaults are merged when loaded. ranking.priorityKeywords, ranking.negativeKeywords, and ranking.minimumHourlyTarget apply on top of every lane. presets adds your own named query lists.
Scores explain relative fit; they are not predictions of winning a job.
A lane is a kind of work. It decides which searches hunt runs and what counts as a strong title match when jobs are ranked. List them:
upwork-cli lanes| Lane | For |
|---|---|
ai-automation |
n8n, Make, Zapier, and AI-powered business workflows |
ai-agents |
Agents, RAG, chatbots, MCP servers, and LLM integrations |
ai-apps |
Full-stack apps, SaaS MVPs, and prototypes built with AI coding tools |
ai-training |
Teaching teams to use AI tools through workshops, courses, and coaching |
ai-consulting |
Advisory, roadmaps, audits, and fractional AI leadership |
ai-creative |
AI video, images, voice, and content production |
Pick one or more by id or number:
upwork-cli setup --lanes 1,2With no lanes selected, every lane competes. Each job is scored against each of your lanes and keeps its best one, so the lane field in JSON output tells you why it ranked. ai-training and ai-consulting also require AI context, so a generic sales trainer or marketing strategist ranks low.
Terms match whole words. rag matches "RAG pipeline" and not "storage". A term ending in * matches as a prefix, so automat* matches automate and automation.
Add it under customLanes in ~/.upwork-cli/config.json, then select it. A custom lane with a built-in id replaces that lane.
{
"lanes": ["shopify-ai"],
"customLanes": {
"shopify-ai": {
"label": "Shopify AI",
"titleTerms": ["shopify", "ecommerce"],
"contextTerms": ["ai", "chatbot*", "gpt*"],
"requireContext": true,
"priorityKeywords": ["shopify ai"],
"negativeKeywords": ["theme customization"],
"queries": ["shopify ai", "shopify chatbot", "ecommerce ai automation"]
}
}
}Run upwork-cli lanes --queries to see every lane’s searches. Lane improvements are welcome as pull requests.
Start with a small live pull:
upwork-cli doctor
upwork-cli hunt --pages 1 --sort recent --max-age-hours 168 --inspect-top 25 --eligible-onlyhunt with no argument runs every query in your lanes. Name one lane to run only its queries, for example upwork-cli hunt ai-agents. Supply your own phrases when you need a narrower hunt:
upwork-cli hunt --queries "Claude Code,Claude Cowork,MCP server" --pages 1 --sort recent
upwork-cli search "AI workshop" --pages 2 --sort recent --format markdown --output shortlist.md
upwork-cli feed recent --batches 3 --max-age-hours 48Results go to stdout; progress and run totals go to stderr. Use JSON when piping to another program:
upwork-cli hunt ai-consulting --format json --output jobs.json
upwork-cli history stats
upwork-cli history runs --limit 10
upwork-cli history trends --days 30
upwork-cli history queries --days 30Search, hunt, and feed save all observed jobs, including refreshed records. By default they output only jobs first seen in that pull. --all includes previously seen jobs. cache reads stored jobs without a live search.
upwork-cli hunt ai-automation --pages 1 --all
upwork-cli cache --format csv --output cached-jobs.csvNet-new describes the database comparison, not when the client published the job. Use recency filters as well.
Only the highest-ranked --inspect-top candidates receive fresh full-detail inspection. Cached detail eligibility expires after 24 hours by default, controlled by eligibility.maxDetailAgeHours. It is recomputed for your current country. Jobs outside the inspected group can have unknown eligibility. The normal filter removes confirmed incompatible jobs; unknown eligibility can still appear in discovery output.
Use --eligible-only for an actionable shortlist. It requires both a fresh detailInspectedAt and eligibleForProfile === true. Check the title and description for hard restrictions too. The parser recognizes standard platform location labels and selected explicit country restrictions; it cannot interpret every legal, residency, language, or regional requirement.
Unknown eligibility stays labeled unknown, including when a page was inspected but the restriction could not be resolved. Citizenship requirements need manual verification; residence does not establish citizenship. Missing payment or client-history data is not proof of a bad client. Check the live posting before deciding. --include-ineligible is for inspecting excluded records deliberately.
The starter profile has no name, rate, biography, credentials, results, or proof links. Add your own before drafting:
upwork-cli proposal profile
upwork-cli proposal playbookThese commands print the files to open. Follow the guides in order:
- Voice workbook: capture your vocabulary, cadence, and verified experience.
- Proposal writing: choose evidence, answer screening questions, and edit without flattening your voice.
- Assistant instructions: give your assistant the boundaries for discovery, drafting, review, and browser ownership.
The profile is private. Keep it out of Git and issue reports. The repository’s profile.example.json documents the empty starting structure.
Replace JOB_ID below with the ID or URL of a job you selected. Read-only inspection can avoid opening the application form:
upwork-cli proposal inspect JOB_ID --no-form
upwork-cli proposal packet JOB_ID --no-form --output packet.mdWhen you deliberately start an application, capture its live terms and exact questions:
upwork-cli proposal template JOB_ID --rate 100 --output application.jsonThe rate is an example, not a recommendation. Review every term. For fixed-price work, inspect the generated fixed price, payment mode, and duration. Write the cover letter and each answer without changing the question text.
upwork-cli proposal lint application.json
upwork-cli proposal validate application.json
upwork-cli proposal review application.json
upwork-cli proposal status application.jsonlint reports specific writing patterns and excerpts. It does not verify achievements or detect who wrote the text. Resolve errors and assess warnings in context.
Before anything is submitted, show the user the complete final application: cover letter in one code block, each question followed by its answer in a separate code block, rate, base Connects, boost, and maximum total. State NOT SUBMITTED and wait for exact-content approval.
proposal review prints the cover letter and every answer in separate blocks, ready to paste. Open the job in Upwork, paste each block, set your rate and any boost, and submit. Then record it so your history stays accurate:
upwork-cli proposal record-submitted application.json --proposal-id 1234567890 --confirm "RECORD SUBMITTED 1234567890"The proposal ID is the number in the page address after you submit.
Section 3.5 of Upwork’s Terms of Use prohibits robots, scrapers, and similar mechanisms without written permission. Upwork says it can warn, restrict, or permanently block accounts using unapproved automation. For that reason proposal fill and proposal submit refuse to run until you turn them on for your own installation:
upwork-cli setup --enable-live-proposalsTurn them off with --disable-live-proposals. The choice is yours and so is the account risk. Discovery commands also drive your browser, so keep pulls small and spaced out either way. When enabled, a submit also ticks Upwork’s policy acknowledgement dialog as part of the approved submission.
DRAFT → REVIEWED → FILLED → READY → SUBMITTED
↘ BLOCKED
↘ MANUAL_CONTROL
After approval, proposal fill application.json makes one guarded live attempt and verifies the form. Keep the READY form open and unchanged. Submission uses the exact approval phrase emitted by review and the separate live Connects confirmation. Run upwork-cli proposal submit --help for the required arguments. Never generate approval on the user’s behalf.
Any content or terms change invalidates approval. A successful submission is terminal. A failed live action becomes BLOCKED and must not be retried automatically. Follow the stored nextAction returned by proposal status.
For manual takeover, run proposal handoff application.json first. Use proposal resume application.json only when the user returns control. If they submitted manually, reconcile with proposal record-submitted --help; do not replay the application to find out what happened.
| Location | Contents |
|---|---|
~/.upwork-cli/config.json |
Browser, country, ranking, and search preferences |
~/.upwork-cli/profile.json |
Your private voice and evidence |
~/.upwork-cli/chrome-profile/ |
Dedicated signed-in Chrome profile |
~/.upwork-cli/data/jobs.sqlite |
Job history and observations |
~/.upwork-cli/data/proposal-workflows.json |
Workflow state, checkpoints, and errors |
~/.upwork-cli/data/proposals.json |
Submission audit records |
Chrome stores its own session material. Keep the state directory private. Application exports and packets can contain your writing, client details, and rates. They are not safe to publish by default.
Use UPWORK_CLI_HOME for an isolated state directory. Additional overrides are UPWORK_CLI_CONFIG, UPWORK_CLI_PROFILE, UPWORK_CLI_CHROME, UPWORK_CLI_PORT, and UPWORK_CLI_CDP_TIMEOUT_MS. Set a distinct browser port as well when running separate sessions.
| Symptom | Next step |
|---|---|
| Chrome executable missing | Install Chrome or set UPWORK_CLI_CHROME to its executable |
| Login or verification required | Run upwork-cli auth, complete the step in its dedicated window, then run doctor |
| Your normal browser is signed in but the CLI is not | Sign in to the dedicated profile; do not copy cookies |
| No results on a repeated pull | Inspect history runs; try --all to include previously seen jobs |
| Unknown location eligibility | Set your country, inspect details, and read the live restriction |
proposal fill says live automation is off |
Submit it yourself and run record-submitted, or opt in with setup --enable-live-proposals |
| Proposal is BLOCKED | Read proposal status and perform only its required recovery step |
| Submission outcome uncertain | Check My Proposals manually and reconcile; do not submit again |
| READY checkpoint is stale | Stop and inspect status; do not refresh or refill automatically |
npm ci
npm test
node src/cli.mjs --help
npm run check:releaseRun upwork-cli doctor --offline to check local readiness without connecting to Chrome. Invalid settings and numeric command options fail before browser work.
The tests run offline. They cover parsing, ranking, storage, proposal validation, browser ownership, and workflow state. An end-to-end CLI test exercises setup, diagnostics, import, cache, proposal review, and approval rejection using isolated state. They do not prove the live Upwork UI still matches every selector. Never test submissions using another person’s account or existing proposal state.
Use synthetic fixtures in tests. Do not commit profiles, browser sessions, job exports, proposal drafts, or private handoffs. See CONTRIBUTING.md for the release checks.
See performance and verification for the repeatable cache benchmark and validation scope.