Automated Upwork job discovery. Queries the Upwork GraphQL API on a schedule, filters for jobs matching your profile, and saves them as structured markdown files.
Built to feed into a triage pipeline — you review and apply to jobs that matter, not scroll through hundreds of listings.
- Search — Runs profile-based queries against the Upwork GraphQL API (skills, domains, budget thresholds)
- Filter — Client-side filtering catches what the API misses (budget, stack match, experience level)
- Dedup — Tracks seen job IDs so you never see the same posting twice
- Save — Writes structured markdown files with YAML frontmatter to your output directory
- Schedule — macOS launchd runs the whole thing every 6 hours automatically
- Bun runtime
- Upwork API key (register here)
- macOS (for launchd scheduling)
# Clone and install
git clone https://github.com/ihoka/upwork-search.git
cd upwork-search
bun install
# Configure
cp .env.example .env
# Edit .env with your Upwork API credentials and output directory
# Authenticate with Upwork (one-time)
bun run setup
# Run manually
bun run search
# Run tests
bun testUPWORK_CLIENT_ID=your_client_id
UPWORK_CLIENT_SECRET=your_client_secret
UPWORK_REDIRECT_URI=http://localhost:3000/callback
OUTPUT_DIR=~/Documents/Obsidian/Personal/+Inbox/UpworkScopes on Upwork are configured on the API key, not requested in the authorize URL. If bun run search reports an error like:
The client or authentication token doesn't have enough oauth2 permissions/scopes to access: [Money.currency, ...]
open the Upwork API Center, edit your key, and enable these scopes:
- Common Entities - Read-Only Access (required for all Upwork API calls)
- Read marketplace Job Postings (required for
marketplaceJobPostingsSearchand itsMoney/PageInfo/ nested fields)
After changing scopes, existing access tokens are invalidated — re-run bun run setup to obtain new ones. See docs/upwork/authentication-and-scopes.md for more detail.
Define your search criteria — keywords, categories, and filters:
searches:
- terms: ["React", "TypeScript", "senior"]
category: "Web Development"
- terms: ["billing", "invoicing", "subscription"]
category: "Web Development"
filters:
experienceLevel: "EXPERT"
hourlyBudgetMin: 50
jobType: ["HOURLY", "FIXED"]
clientHiresCount_gte: 1
postedWithin: "24h"Install the launchd plist to run every 6 hours:
# Edit the plist to match your bun path and project directory
cp com.ihoka.upwork-search.plist ~/Library/LaunchAgents/
launchctl load ~/Library/LaunchAgents/com.ihoka.upwork-search.plistCheck logs:
tail -f logs/stdout.log
tail -f logs/stderr.logUninstall:
launchctl unload ~/Library/LaunchAgents/com.ihoka.upwork-search.plist
rm ~/Library/LaunchAgents/com.ihoka.upwork-search.plistEach job is saved as a markdown file with structured frontmatter:
---
source: upwork-api
upwork_job_id: "~01abc123def456"
upwork_fetched: 2026-04-09
upwork_url: "https://www.upwork.com/jobs/~01abc123def456"
---
#### Senior React/TypeScript Developer for SaaS Platform
**Posted:** 2026-04-09T14:30:00Z
**Summary**
[Job description]
- **30+ hrs/week**
Hourly
- **Expert**
Experience Level
- **$60.00 - $120.00**
Hourly
**Skills:** React, TypeScript, Node.js, PostgreSQL
**Client History**
- Total hires: 12
- Total spent: $45,000src/
auth/ # OAuth2 token management
search/ # GraphQL client and query construction
transform/ # API response to markdown conversion
dedup/ # Duplicate detection via JSON state file
config.ts # Environment and path configuration
main.ts # Entry point
tests/ # Unit tests (mirrors src/)
data/ # Runtime state (tokens, seen jobs)
# Run tests
bun test
# Run tests in watch mode
bun test --watch
# Run a single search cycle
bun run search
# Run OAuth setup
bun run setupMIT