Upload your resume and get the latest jobs that fit you, ranked by an AI match score, with matching and missing skills for every job. It runs on your own computer: a local web page drives a real browser (Playwright) through LinkedIn, Naukri and Indeed, and OpenAI reads your resume and scores each job.
job-search-demo.mp4
Screenshots in this guide use sample data.
New here? Follow QUICKSTART.md: every step from installing Python to your first search.
What it does for you
- Reads your resume and fills in keywords, city and an experience range automatically
- Searches LinkedIn, Naukri and Indeed at the same time, only recent posts (12 h to 7 days)
- Scores every job 0-100 against your resume and explains why (matching / missing skills)
- Remembers jobs you applied to or hid, and can show only new jobs
- Runs every day at a time you choose, even when the page is closed
- Keeps your page as it was after a reload or restart, until you press Clear
- Saves work it already did (job details, AI scores, resume reading), so repeat runs are faster and cheaper
- Install (one time)
- Start the app
- How to use the website, step by step
- Tips and troubleshooting
- Settings in
.env - Command line (optional)
- Where your data is stored
- For developers
Want to help? See CONTRIBUTING.md for the branch workflow and coding rules.
You need Python 3.10+, Node.js 20+ (for the web page), Git and an OpenAI API key.
git clone https://github.com/pattjoshi/playwright-python-job-search.git
cd playwright-python-job-search
python -m venv .venv
# Windows: .venv\Scripts\activate
# macOS/Linux: source .venv/bin/activate
pip install -r requirements.txt # the backend (Python) and its packages
playwright install chromium # the browser that searches the job sites
cd frontend
npm install # the web page's packages
npm run build # builds the page into frontend/out
cd ..
# Windows: copy .env.example .env macOS/Linux: cp .env.example .envYour OpenAI key: the easiest way is to start the app (step 2), sign in and open Settings → OpenAI key: paste
it and press Save key. The app checks it with OpenAI first, saves it encrypted in data/secrets.json and never
shows it again (only sk-…abcd). It's used at once, also by the daily search; Remove goes back to .env.
Or put it in .env instead:
OPENAI_API_KEY=sk-....env and data/ are never uploaded to GitHub, so your key stays on your computer. For extra safety set
APP_SECRET in .env (any long random text): the saved key is then encrypted with it instead of data/secret.key,
so a copy of the data/ folder alone can't reveal it.
python -m job_searchOn Windows you can also just double-click start_ui.bat (macOS/Linux: ./start_ui.sh). It builds the
page the first time if needed.
Your browser opens http://127.0.0.1:8000. Keep the terminal window open while you use the page; press Ctrl+C in it to stop the app. The page only works on your computer (it isn't on the internet).
First time: the page asks you to create your account (name, email, password). It protects your resume,
jobs and history on this computer. Next time you just sign in. Forgot the password? In the project folder run
python -m job_search reset-password --email you@example.com.
http://127.0.0.1:8000 shows the home page; click Get started or Open dashboard. After signing in, the app opens on the Dashboard (your counts, the latest matches, the daily search and recent searches).
Use the sidebar to move between pages; the light/dark switch is at the top right.
Open Search jobs in the sidebar. The left side is where you set up the search; the right side shows progress and results. A running search keeps going if you open another page: the top bar shows a timer, and a message pops up when it's done.
- Drag your resume onto the box, or click it and choose the file (PDF, DOCX or TXT).
- Wait a few seconds while the AI reads it.
- The card shows your file name, current title, years of experience and a one-line summary.
- Same resume again? The AI reading is reused: no extra AI cost.
- After a reload or app restart the resume is still there (a green note says it was restored). You don't need to upload it again.
- To use a different resume, click Replace.
- Keywords: filled in from your resume as 6–10 short job titles: your main title plus the other names the same job is posted under (for example GenAI Engineer, LLM Engineer, AI Engineer), so you don't miss posts. Type a new one and press Enter to add it; click × to remove one. Keep them to short job titles: Naukri wants every word to match, so "GenAI Engineer Python FastAPI LangChain" finds almost nothing (the page warns you). Your skills are used for scoring, not searching. Fewer keywords = faster search.
- Locations: click the box and pick one or more cities, or Remote (work from home).
Type to filter (e.g.
hyd+ Enter picks Hyderabad). You can also type any other city and choose Add "…". Leave it empty to use the city from your resume. - Experience: e.g. 2 to 4 years. Jobs asking for clearly more or less are filtered out. Tick Any to switch this off.
- Exclude companies: type a company and press Enter (or several with commas, e.g.
TCS, Wipro). Their jobs are left out of every search, the daily search and the results on screen. "Infosys Ltd" and "INFOSYS" count as the same company. It's the same list as Hide this company in the results, so a company you hide there shows up here too; click × to show its jobs again. - Posted within: 12 hours, 24 hours, 3 days or 7 days.
- Turn each site on or off with its switch.
- Use − / + (or type) to choose how many jobs to show from that site (1-100), e.g. LinkedIn 20, Naukri 12, Indeed 10.
- Only new jobs: skip jobs you were already shown in earlier searches.
- Show browser: watch the search happen. Keep this on if Naukri or Indeed ask you to "verify you're human": you can solve it in the browser window and the search continues.
- Advanced:
- Pages per search: how deep to look on each site (default 3).
- Jobs to score per site: how many jobs the AI checks per site (default 40). Higher = slower and a bit more cost.
- Forget seen jobs: makes previously shown jobs count as new again (applied/hidden jobs stay).
- Check the estimate above the button (e.g. "24 searches across 3 sites · about 5 min").
- Click Start search.
- Watch the progress card: the steps (Resume → Search → Details → Scoring → Done), each site's status and how many jobs it found, and a timer.
- Click Stop search at any time to cancel.
The line of numbers is the funnel: how many jobs were left after each filter, e.g.
184 found → 131 unique → 102 posted in 24h → 61 match 2-4 yrs → 38 scored. If you get fewer jobs than
you asked for, a yellow hint tells you which filter removed the most and what to change.
- The header shows how many jobs are new, the run time, AI tokens used (and cost, if configured) and how many scores were reused from earlier runs.
- Before opening jobs, the search drops loose matches the boards add (a "Sales Executive" for "AI Engineer": the title shares no word with your keywords and the job mentions none of your skills), merges the same job posted on two sites ("Sr. AI Engineer" at "Acme Pvt Ltd" = "Senior AI Engineer" at "ACME"), and ranks equal scores newest first.
- Tabs: All (best matches from every site) or one site at a time.
- Filters (no new search needed): type words to match the title, company or location, pick a minimum score (60+ / 70+ / 80+) or how recently it was posted, and sort by Best match, Newest or Company A–Z. The line under them says how many jobs the filters left out; Clear shows them all again.
- Each job card shows:
- the match score (green 75+, yellow 50-74, grey below 50)
- title, company, location, required experience, when it was posted
- a New badge if you haven't seen it before
- Matching skills (green) and Missing skills
- a one-line reason for the score
- Buttons on each job:
- Details: opens a side panel with the job description, the AI's reason, matching and missing skills, and the same actions, plus Hide this company
- View job ↗: opens the job on the site, where you apply
- Track: adds the job to Applications as Saved, so you can follow it there
- ✓ Mark applied: tracks it in My jobs and Applications (as Applied) and never shows it again
- Hide: never show this job again (Undo is right there)
- Hide this company (in Details) removes that company's jobs from the results and from every future search ("Acme Pvt Ltd" and "ACME" count as the same company). Hidden companies (n), next to the filters, lists them so you can show one again.
- Open report opens the same results as a standalone HTML page; Download Excel gets every scored job.
On the Search jobs page, click My jobs (top right of the page).
- Applied and Hidden tabs list those jobs with links and dates. Undo moves a job back.
- Search history lists every search (manual and daily): status, time taken, jobs found per site, how many were shown/new, tokens and cost, the funnel, any error, and a link to its report.
- Set up the search above the way you like it (resume, keywords, cities, sites…).
- In Daily search, choose a time (e.g. 9:00 AM) and the days (or click Every day / Weekdays).
- Click Save daily search.
- It runs through Windows Task Scheduler (cron on macOS/Linux), so it works even when the page is closed. If the laptop was off or asleep at that time (Windows), it runs as soon as it's back on.
- When it finishes, the report opens in your browser (untick Open report when ready if you don't want that).
- The card shows the next run, the last run with a link to its report, and a Turn off button.
- Changed your settings? Click Update daily search.
- Test it without waiting:
python -m job_search daily(log:data/scheduled.log).
- Reloading the page or restarting the app keeps everything: your resume, keywords, the last search's progress and results (including jobs you marked applied).
- Click Clear (top right of the Search jobs page) to remove the resume, keywords and results from the page. It asks you to confirm first. Your settings, applied/hidden jobs, search history and daily search are kept.
Click Applications in the sidebar.
- Board: one column per stage (Saved, Applied, Screening, Interview, Offer, Rejected, Withdrawn). Drag a card to another column to move it (or focus it and use Space, the arrow keys and Space).
- Table: the same applications as rows. Click a column heading to sort, and filter by status.
- Add application for jobs you found elsewhere (company, role, link, location, salary, contact, applied and follow-up dates, notes). Import applied jobs adds the jobs you already marked applied.
- Click a card or row to open its details: change the status, open the job, see its timeline, Edit or Delete it.
- The Dashboard shows how many applications are in each stage, how many you applied to each day for the last two weeks, and your next follow-ups (overdue ones in red).
Click Notes in the sidebar: Notion-style pages for company research, interview prep and plans.
- New page, give it a title and an icon. Hover a page in the list and press + to put a page inside it (e.g. Interview prep → Infosys → Round 1). Use Move to above a page to move it, or the bin to delete it (the pages inside go with it).
- Type / in a line for the block menu, or use Markdown shortcuts at the start of a line:
| Type | Gets you |
|---|---|
# ## ### |
Headings |
- |
Bulleted list (Enter continues it, Enter on an empty item ends it) |
1. |
Numbered list |
[] |
To-do with a checkbox |
> |
Quote |
``` |
Code |
--- |
Divider |
- Drag the ⋮⋮ handle to reorder blocks. Backspace at the start of a line turns it back into text, then joins it to the line above. Everything saves by itself ("Saved" at the top right).
| What you see | What to do |
|---|---|
| 0 jobs from a site | Check the funnel line and the yellow hint. Try more cities, fewer filters, Any experience or a longer Posted within. Click Show log for details. |
| "verify you're human" on Naukri or Indeed | Keep Show browser on and solve it in the browser window; the search continues. |
| Naukri: blocked the search (log: Access Denied) | Naukri refused the automated browser, so the app skipped it for that run instead of waiting on every search. Turn on Show browser and search again. Having Google Chrome installed helps: the app uses it when it's there. |
| A site shows couldn't load ✕ | The site was down or blocking for now; the other sites still ran. Try again later. |
| "Add your OpenAI key first" | Save it in Settings → OpenAI key (or put OPENAI_API_KEY in .env and restart). |
| "Could not read your resume" | Use a text-based PDF or DOCX (not a scanned image). |
| "Upload your resume first" | Your saved resume file was cleaned up or cleared; upload it again. |
| Searches are slow | Fewer keywords or cities, fewer Pages per search, or a lower Jobs to score per site. Repeat runs are faster because work is reused: a keyword + city searched on a site in the last 2 hours isn't searched there again, and a search stops early once a page only shows jobs already found. |
| A search found nothing new right after the last one | Results from the last 2 hours are reused. Wait a little, or delete data/cache.sqlite3 to search every site live. |
| Too many browser windows | That's one per site while searching in parallel. Set PARALLEL_SITES=false in .env to use one at a time. |
| Want to see the cost in $ | Add your model's prices to .env (OPENAI_PRICE_INPUT, OPENAI_PRICE_OUTPUT). |
Be polite to the job sites. The tool waits between requests, retries when a site says "too many requests", and gives up on a site that keeps failing. Job boards restrict automated access, so keep this for personal, low-volume use.
The web page remembers your choices on its own. These .env settings are defaults (and are used by the
command line). See .env.example for the full list with comments.
| Setting | Default | Meaning |
|---|---|---|
OPENAI_API_KEY |
(or Settings) | Your OpenAI key; a key saved in Settings → OpenAI key is used instead |
APP_SECRET |
(optional) | Encrypts the key saved in Settings (otherwise data/secret.key is used) |
OPENAI_MODEL |
gpt-5.4-mini |
Model used to read resumes and score jobs |
RESULTS, RESULTS_LINKEDIN, RESULTS_NAUKRI, RESULTS_INDEED |
10 | Jobs shown per site |
LOCATION |
from resume | Cities, comma separated (Pune, Bengaluru, Remote) |
KEYWORDS |
from resume | Search phrases, comma separated |
EXPERIENCE |
any | Years wanted, e.g. 1-2 |
HOURS |
24 | Only jobs posted within this many hours |
ONLY_NEW |
false | Skip jobs shown before |
SITES |
all | linkedin, naukri, indeed |
TOP / MAX_PAGES |
40 / 5 | Jobs scored per site / result pages per search |
PARALLEL_SITES |
true | Search sites at the same time |
KEEP_DAYS |
30 | Delete old uploads, reports and cached data after this many days (0 = never) |
OPENAI_PRICE_INPUT, OPENAI_PRICE_OUTPUT, OPENAI_PRICE_CACHED_INPUT |
empty | USD per 1M tokens, to show cost per run |
Everything the page does is also available without it:
python -m job_search search --resume my_resume.pdf --location Pune Remote --experience 2-4 --results linkedin=20 naukri=12Run python -m job_search search --help for all options. Each option matches a .env setting; the command line wins.
Everything stays in the project folder on your computer:
| Folder / file | What's in it | Removed by |
|---|---|---|
uploads/ |
Resumes you uploaded | KEEP_DAYS cleanup (never the one in use) |
output/ |
HTML / Excel / CSV reports | KEEP_DAYS cleanup (never the one on screen) |
data/history.sqlite3 |
Your account, applications, notes, applied / hidden / seen jobs and the search history | Never automatically |
data/cache.sqlite3 |
Saved job details, AI scores, resume readings, searches from the last 2 hours | KEEP_DAYS cleanup; safe to delete anytime |
data/session.json |
What the page shows (resume, last results) | The Clear button |
data/search-filters.json |
Companies you hid from searches | Hidden companies list next to the result filters |
data/schedule.json, data/scheduled.log |
Daily search settings and its log | Turn off / log is trimmed when large |
Your resume text and job descriptions are sent to OpenAI for reading and scoring; nothing else leaves your computer.
Descriptions are trimmed first: the company intro, benefits, equal-opportunity text and "apply now" lines are left out,
and at most 1,200 characters of the job itself are sent. The dashboard's Recent searches shows the AI tokens
each run used (and the cost, if you set OPENAI_PRICE_INPUT / OPENAI_PRICE_OUTPUT in .env).
Updating from a version with Auto-apply / Answer Bank? They were removed. On the next start the app deletes their
saved data from data/history.sqlite3 (back up data/ first if you want to keep your answers). The folders
data/auto-apply/ and data/browser-profile/ and the NAUKRI_/LINKEDIN_/INDEED_EMAIL and _PASSWORD lines in .env
are no longer used: delete them. In Chrome, remove the "Job Search Auto-apply" extension on chrome://extensions.
- It only listens on your own computer (
127.0.0.1) and only answers requests addressed to127.0.0.1orlocalhost, so other devices on your Wi-Fi can't reach it, and a website can't reach it either, even with tricks like DNS rebinding. (Need another name, e.g.my-pc.local? Add it toALLOWED_HOSTSin.env.) - Everything except sign-in needs your password (scrypt-hashed; 5 wrong tries pause sign-in for a while). Changes must come from the app's own page: other websites, and other pages on your computer, are refused.
- Job details come from the job sites and are treated as untrusted. They're shown as plain text, links must be
http(s), and Excel/CSV cells can't turn into formulas. - Secrets are never shown or logged: your OpenAI key only appears as
sk-…abcd. - Your data in
data/,uploads/andoutput/isn't encrypted (only the saved OpenAI key is). Anyone who can open your user account's files can read it, so keep your computer account protected.
The project has two parts: a Python backend (FastAPI + Playwright + OpenAI) and a Next.js frontend (TypeScript). The frontend is exported as static files that the backend serves, so users run one command.
backend/ Python package `job_search` (pip install -r requirements.txt installs it)
src/job_search/
api/ FastAPI app (app.py) and one router per resource (routes/)
cli/ commands: serve (default), search, daily
config/ settings from .env, project paths, city list
models/ plain dataclasses: Job, Profile, SearchOptions, Schedule...
database/ SQLAlchemy models + Alembic migrations (run automatically on start)
repositories/ job/run history, users, applications, answers (SQLAlchemy), cache, schedule, session
schemas/ API errors, request validation, response serializers
services/
search/ the search: runner -> pipeline -> sites / filters / profile
scraping/ one browser per site + linkedin.py, naukri.py, indeed.py
reporting/ HTML / Excel / CSV reports
llm_service.py OpenAI: resume reading, parallel job scoring, token usage
search_filters.py companies you hid (left out of every search)
applications.py application tracker: validation, status changes, tracking jobs from results
search_manager.py runs one search at a time in the background for the web page
schedule_service.py daily search (Windows Task Scheduler / cron)
tests/ unit/ (fast) and integration/ (headless browser, fakes)
frontend/ Next.js + TypeScript + Tailwind CSS + shadcn/ui (static export)
src/app/(app)/ one folder per page: dashboard, search, applications, settings...
src/components/ ui/ (shadcn/ui + small custom parts), layout/ (sidebar, top bar), one folder per feature
src/providers/ app-wide state: config, theme, the search session (survives page changes)
src/hooks/ state and API calls (search form, running search, resume...)
src/lib/ typed API client, types, formatting, estimate, storage
src/styles/theme.css design tokens (colours, radius) for light and dark mode
.github/workflows/ci.yml lint, type check, tests and build on every pull request
Develop with live reload (two terminals):
python -m job_search --no-open # backend on http://127.0.0.1:8000
cd frontend && npm run dev # page on http://localhost:3000 (API calls go to :8000)Checks (the same ones CI runs):
pip install -e "./backend[dev]"
cd backend && ruff check . && pytest # backend
cd frontend && npm run lint && npm run typecheck && npm test && npm run build # frontend
python scripts/check_file_length.py # every code file under 250 linesAccounts and access: every /api and /reports request needs a signed-in user (session cookie;
/api/auth/* is the only public part). Owner-only actions use require_owner(request) from api/auth.py.
Database changes: edit backend/src/job_search/database/tables.py, then create a migration with
cd backend && alembic revision --autogenerate --rev-id 0002 -m "what changed". The app applies it on start.
New UI components: shadcn/ui components are copied into frontend/src/components/ui/
(npx shadcn@latest add <name> from frontend/). Keep each file under 250 lines.
Tests never touch the real sites or OpenAI. To add a job board, create
backend/src/job_search/services/scraping/<board>.py with a class extending BaseScraper (implement
search() and, if needed, fetch_description()) and register it in services/scraping/__init__.py.











