This project refactors a single Greenhouse collector into a reusable multi-board pipeline.
- Greenhouse
- Lever
- Ashby
- SmartRecruiters
- Workable
- Recruitee
- BambooHR
- Generic JSON-LD
JobPostingpages
Each adapter is one Python file under boards/. Every adapter returns the same
NormalizedJob model. Shared PostgreSQL, Gemini enrichment, search/filter API,
and UI logic remain centralized instead of being copied into every adapter.
Job-board API
-> board adapter
-> NormalizedJob validation
-> PostgreSQL upsert
-> Gemini structured enrichment
-> FastAPI search/filter/save API
-> browser UI
Windows PowerShell:
python -m venv .venv
.\.venv\Scripts\Activate.ps1
pip install -r requirements.txtCopy-Item .env.example .env
Copy-Item boards.example.json boards.jsonEdit .env and boards.json.
psql -U postgres -d careeros -f schema.sqlThe schema is compatible with the earlier opportunities table and adds the
columns needed for Gemini enrichment and saved opportunities.
boards.json is a list. Enable only adapters with valid board identifiers or
credentials.
Public adapters normally do not need credentials:
- Greenhouse
- Lever
- Ashby
- Recruitee
- Generic JSON-LD
- Workable public careers endpoint
Some configurations may require credentials:
- SmartRecruiters
- Workable SPI
- BambooHR
Secret values can reference environment variables:
{
"api_key": "env:SMARTRECRUITERS_API_KEY"
}Run every enabled board:
python ingest.pyRun one adapter:
python ingest.py --board greenhouseIngest and then process 20 records with Gemini:
python ingest.py --process --process-limit 20python gemini_pipeline.py --limit 20Gemini returns schema-constrained JSON, and Pydantic validates the response before it is saved.
Generated fields:
- summary
- skills
- category
- work mode
- eligibility
- deadline
uvicorn main:app --reloadOpen:
http://127.0.0.1:8000
The UI includes:
- text search
- category filter
- skill filter
- work-mode filter
- extracted summary and skill display
- save/unsave opportunity
- saved-only view
Create one file under boards/ with:
def fetch(config: dict) -> list[NormalizedJob]:
...Then register it in boards/__init__.py.
No implementation can safely ingest literally every job board through one method. Large marketplaces such as LinkedIn, Indeed, and Naukri do not provide the same unrestricted public job-feed APIs as the supported ATS platforms. Use their approved partner APIs, licensed feeds, or user-authorized exports rather than scraping protected pages.