JobGraph is an agentic job-discovery system for senior AI engineers. It ingests postings from high-signal sources, scores each against a structured candidate profile using a LangGraph pipeline, and delivers a daily email digest of top opportunities. Built on Python 3.12, LangGraph, and PostgreSQL; deployed on GCP Cloud Run.
Full design in docs/architecture.md; significant decisions
are recorded as ADRs in docs/decisions/.
ingest → score (LangGraph) → daily digest
- Ingest — pull postings from curated sources (MVP: agentic-engineering-jobs.com),
dedup by
sha256(normalize(company) + normalize(title)), and persist the source's structured fields (locationType,geoRegion,seniority, salary, tech tags, …) inpostings.source_metadata. - Score — a LangGraph pipeline: deterministic hard filters → fact extraction
(facts derived from the structured metadata, LLM only for domain tags + yellow
flags) → remote-eligibility filter → dimension scoring → aggregate to a 0–100 score
and a
recommend/consider/skip/hard_rejectdecision. See ADR-0002 and ADR-0003. - Digest — render the top postings to markdown, convert to HTML, and send a daily email (SMTP + STARTTLS).
The LLM provider is configurable via .env; the MVP runs on OpenAI
(ADR-0001).
Prerequisites: Python 3.12+, uv, Docker (for Postgres).
# 1. Install dependencies
uv sync --group dev
# 2. Configure — copy the template and fill in your keys
cp .env.example .env
# set LLM_PROVIDER / LLM_MODEL_* (OpenAI by default), OPENAI_API_KEY,
# and the SMTP_* / DIGEST_* values for email delivery
# 3. Start Postgres and run migrations
docker compose up -d postgres
uv run alembic upgrade head
# 4. Smoke-test LLM connectivity
uv run python scripts/smoke_llm.py # prints PONG# Ingest one source
uv run jobgraph ingest --source agentic-engineering-jobs
# Full pipeline: ingest → score → send digest (if due)
uv run jobgraph run
# Force-send the digest now (bypasses the 24h guard)
uv run jobgraph digest send
# Reset all data to run like the first time (prompts; --yes to skip)
uv run python scripts/reset_db.pymake test # pytest (DB tests use testcontainers — Docker must be running)
make lint # ruff check
make format # ruff format
make typecheck # mypyDB tests spin up a throwaway Postgres via testcontainers. On some Docker setups you may need to export
DOCKER_HOST(your Docker socket) andTESTCONTAINERS_RYUK_DISABLED=true.
src/jobgraph/
config/ pydantic-settings (.env)
llm/ provider-agnostic factory over init_chat_model()
db/ asyncpg + raw-SQL repositories
ingestion/ source ingestors (RawPosting → postings)
scoring/ LangGraph pipeline (graph, nodes, models)
digest/ render (markdown→HTML) + SMTP delivery
pipeline/ orchestration (ingest → score → digest)
migrations/ Alembic revisions
scripts/ smoke_llm.py, reset_db.py
docs/ architecture.md, decisions/ (ADRs)