Threadline is an AI-powered job application tracker that turns job postings and Gmail replies into a structured, automatically updated application pipeline.
🤖 Extract the job → 📧 read the reply → 🧠 classify the outcome →
⚠️ flag uncertainty
Threadline started as a simple job tracker, but became a practical exercise in building an end-to-end AI system that knows when it might be wrong.
The interesting part isn't just:
"Can AI classify an email?"
It's:
"Can AI automate the boring parts without silently making bad decisions for me?"
1. Paste a job posting
A LangGraph agent extracts the company, role, recruiter details, and application link.
2. Track everything locally
Applications are stored in a local SQLite database with full CRUD support.
3. Check Gmail for updates
Threadline searches Gmail and extracts the full email body from the MIME structure (text/plain + text/html).
4. Classify the outcome
A fine-tuned DistilBERT model classifies emails as:
interview · rejected · test · offer
5. Don't trust the model blindly
Status updates only happen when confidence ≥ 0.35. Otherwise → needs review.
┌──────────────────┐
│ Streamlit │
│ UI │
└────────┬─────────┘
│
┌────────▼─────────┐
│ Backend Logic │
└─────┬─────┬──────┘
│ │
┌─────────▼┐ └─────────────┐
│ SQLite │ │
└──────────┘ ┌────────▼────────┐
│ AI Services │
│ │
│ LangGraph + │
│ Gemini + │
│ DistilBERT │
└────────┬────────┘
│
┌────────▼────────┐
│ Gmail API │
│ OAuth + Search │
└─────────────────┘
A two-node LangGraph:
extract_fields → conditional edge → save_to_db
Gemini extracts structured job information. If extraction fails, nothing is written to the database.
Fine-tuned DistilBERT trained on ~68 hand-labeled emails.
Email
↓
DistilBERT
↓
Prediction + confidence
↓
confidence ≥ 0.35 ?
├── YES → update status
└── NO → needs review
Unambiguous phrases such as "unfortunately, we will not be moving forward" use deterministic overrides instead of the model.
| Component | Technology |
|---|---|
| Agents | LangGraph + Google Gemini (gemini-2.5-flash) |
| Classifier | HuggingFace Transformers + DistilBERT + PyTorch |
| Evaluation | Custom eval harness + hand-verified ground truth |
| Backend | Python + SQLite |
| Frontend | Streamlit |
| Gmail API + OAuth 2.0 + BeautifulSoup4 |
Extraction accuracy: 80% field-level
Classifier dataset: ~68 labeled emails
Random baseline: ~25% for 4 classes
Threadline is local-first.
- 📦 Applications, CVs/cover letters, OAuth token → local machine
- 🧠 Raw email content → processed in memory only
- 💾 Only the derived status label is persisted
- 🔑 Gmail scope →
gmail.readonly - 🚫 Cannot send, delete, or modify emails
- 🌐 External services → Gemini API + authorized Gmail API
This follows the core GDPR principle of data minimization (Art. 5(1)(c)).
- Small training set: ~68 emails → not enough for highly confident predictions on ambiguous text.
- No
not_relevantclass: keyword-based relevance filtering is currently used as a stopgap. - Keyword-based Gmail search: can surface irrelevant results; semantic retrieval is a future improvement.
- No email idempotency: Gmail checks re-process emails rather than tracking processed message IDs.
- No automatic application detection: applications must currently be added explicitly.
- Local-only: no hosted/multi-user version.
These limitations are also why the confidence gate and deterministic overrides exist.
- Add
not_relevanttraining class - Track processed email IDs +
accept/dismiss - Add embeddings-based semantic email retrieval
- Detect new applications from confirmation emails
- Hosted multi-user version with FastAPI + Postgres + OAuth verification
git clone <your-repo-url>
cd Threadline
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txtCreate .env:
GOOGLE_API_KEY=your_key_here- Create a Google Cloud project
- Enable the Gmail API
- Configure OAuth consent and add yourself as a test user
- Create OAuth credentials of type Desktop app
- Save them as
data/credentials.json
The first time you use "Check Gmail for updates", a browser window will prompt you to authorize access. A token.json will then be saved locally.
Note: Unverified apps have refresh tokens that expire every 7 days. This is a Google restriction, not a bug.
streamlit run app.py
Python · LangGraph · Gemini · DistilBERT · PyTorch · Streamlit · SQLite · Gmail API