AI-assisted recruitment screening for internal HR teams.
Publish a job, collect PDF resumes, score and rank candidates with a written justification, ask questions over stored CVs, then draft outreach that a human must approve before anything is sent.
Built as an internship / engineering deliverable for Orange Tunisia. React handles the HR interface. n8n owns the business logic. PostgreSQL + pgvector stores records and embeddings.
- What it does
- Architecture
- Tech stack
- End-to-end flow
- Product screens
- n8n workflows
- Data model
- Repository layout
- Local setup
- Design choices
| Area | Outcome |
|---|---|
| Job offers | HR publishes a role (title, department, description, skills). The offer is stored and embedded for later matching. |
| Applications | Candidates apply on a shareable HTML form (not the React app). They pick an open job and upload a PDF CV. |
| Scoring | Resume text is extracted, a match score (0–100) is generated with a justification, and the candidate is ranked. |
| Dashboard | Volume and quality at a glance: applications, open jobs, average score, strong profiles (≥ 70), awaiting analysis. |
| Chat RH | Free-text questions over resumes (RAG: embed → similar CVs → grounded answer). |
| Outreach | On-demand recommendation + editable email. SMTP send only after Approve and send. |
| Purchase assistant | Side module: describe a need, search the web, get compared product options. |
Actors
- HR / recruiter — React app (dashboard, offers, candidates, chat, purchase).
- Candidate — public apply form and confirmation page only.
- Operator — n8n, credentials, and the database (outside the product UI).
There is no login on the HR UI yet. Anyone who can open the app can use it. Plan accordingly.
Candidate (browser) HR (browser)
│ │
│ HTML form │ React + Vite (port 5173)
▼ ▼
n8n webhooks (port 5678)
apply · score · rank · chat · recommend · email · dashboard · jobs
│
┌───────────────┼───────────────┐
▼ ▼ ▼
PostgreSQL Groq (LLM) Mistral (embeddings)
+ pgvector SMTP Tavily (purchase only)
The frontend is thin: it displays data and triggers webhooks. There is no custom API server. n8n is the backend.
| Layer | Role | Typical local port |
|---|---|---|
| React (Vite) | HR SPA | 5173 |
| n8n | Workflows, AI calls, email, SQL | 5678 |
| PostgreSQL + pgvector | Jobs, candidates, scores, vectors | often 5434 in Docker |
| Candidate form | Public apply experience | http://localhost:5678/webhook/candidate-form |
| Tool | Why it is here |
|---|---|
| React 18 + Vite + React Router | Fast SPA for dashboards and forms |
| n8n | Visible, editable pipelines instead of a custom backend |
| PostgreSQL + pgvector | Relational data and semantic search in one store |
| Docker | Repeatable Postgres + pgvector (especially on Windows) |
| Groq | Scoring, justifications, chat answers, recommendations, purchase analysis |
| Mistral embeddings | Vectors for jobs, CVs, and chat questions |
| Tavily | Live web search for the purchase assistant |
| SMTP (via n8n) | Candidate email after human approval |
- HR creates a job. Title + description + skills are saved and embedded.
- HR copies the apply link from the Candidates page and shares it.
- A candidate selects an offer and uploads a PDF.
- n8n extracts resume text, inserts the candidate, embeds the CV, scores it against the job, stores the score, updates status.
- The candidate sees a confirmation page.
- HR reviews ranked profiles on the Candidates page and metrics on the Dashboard.
- Optionally: Chat RH for questions like “who has AWS or Azure?”
- Optionally: generate a recommendation, edit the email, Approve and send.
- The candidate is marked contacted.
Screenshots below follow that loop: HR tools first, then the public apply path, then ranking and outreach, then chat and the purchase side-module.
Realtime snapshot: applications received, active offers, average score, strong profiles, awaiting analysis.
HR publishes a role. It is immediately available on the candidate form.
Published offers with skills as chips:
The Candidates page exposes the webhook form URL (copy for LinkedIn or email).
Served by n8n, not by React. Open jobs are loaded dynamically.
After submit:
Score out of 100 plus a written justification. Strong vs weak matches are easy to compare.
AI proposes a decision note and a draft. HR edits, then approves.
After send:
Contacted state on the list:
Ask a free-form question over stored resumes.
Describe a need (example: laptop, budget 1500 TND). The workflow searches the web and returns compared options.
Each HR action maps to a webhook. Workflows are the source of truth for scoring, search, and email.
GET /webhook/candidate-form
Load open jobs from Postgres → generate HTML → return the form.
POST /webhook/offer-create — insert offer → Mistral embedding → update vector.
GET /webhook/offer-list — return stored jobs to the React Offers page.
POST receive-application webhook.
PDF extract → insert candidate → Mistral embedding → load job context → Groq scoring → insert match score → update status → confirmation HTML.
Variant with Groq scoring node labeled explicitly:
GET /webhook/dashboard-stats
Aggregate counts and averages for the home page.
GET /webhook/top-candidates?offer_id=…&top_n=…
Ranked list for the Candidates page.
POST /webhook/generate-recommendation
Load candidate + job context → Groq draft (decision, note, email) → persist → return to UI.
POST /webhook/send-email
Load candidate email → SMTP send → mark recommendation sent → update candidate status → confirm.
POST /webhook/ask
Mistral embed of the question → pgvector similarity on resumes → Groq answer grounded in retrieved CVs.
POST /webhook/purchase-assistant
Tavily web search → LLM structures ~3 options (name, price, pros/cons, source URL) → React cards.
Alternate analysis node (gpt-oss on Groq):
Defined in src/api/n8n.js:
| Action | Method | Path |
|---|---|---|
| Create offer | POST | /webhook/offer-create |
| List offers | GET | /webhook/offer-list |
| Top candidates | GET | /webhook/top-candidates |
| Dashboard stats | GET | /webhook/dashboard-stats |
| Chat RH | POST | /webhook/ask |
| Generate recommendation | POST | /webhook/generate-recommendation |
| Send email | POST | /webhook/send-email |
| Purchase assistant | POST | /webhook/purchase-assistant |
| Candidate form | GET | /webhook/candidate-form |
Base URL in the UI: http://localhost:5678/webhook.
Core tables (Postgres + pgvector, managed alongside n8n):
| Table | Purpose |
|---|---|
offers |
Job post, skills, status (open / in_review / closed), embedding |
candidates |
Applicant, resume text, optional file path, embedding, status (new → scored → …) |
match_scores |
Score 0–100 + justification, linked to one candidate and one offer |
Embeddings live next to business rows. HNSW indexes on offers.embedding and candidates.embedding support cosine similarity.
Recommendations (decision type, note, email subject/body, approval, sent-at) are stored by the recommendation / email workflows. Keep the live n8n schema aligned with these tables before a production handoff.
.
├── src/
│ ├── api/n8n.js Single n8n client
│ ├── pages/ Dashboard, Offers, Candidates, Chat RH, Purchase
│ └── components/ Layout, candidate detail
├── docs/ App logos (icon + lockup) and screenshots
├── package.json
└── README.md
n8n workflow JSON and credentials live in the local n8n instance, not in this repo. Export them if you want versioned pipelines next to the UI.
Prerequisites: Node.js, Docker (Postgres + pgvector), n8n, API keys for Groq and Mistral (Tavily if you use purchase), SMTP for outreach.
- Start PostgreSQL with the
vectorextension and create the offers / candidates / match_scores tables (with embeddings). - Start n8n. Configure Postgres, Groq, Mistral, SMTP (and Tavily) credentials. Activate the workflows above and keep webhook paths in sync with
n8n.js. - Run the UI:
npm install
npm run dev- Open
http://localhost:5173for HR. Sharehttp://localhost:5678/webhook/candidate-formwith candidates.
Secrets stay in n8n. Do not put API keys in the React app.
- Human-in-the-loop send — AI drafts; a person clicks send. Protects brand and candidate experience.
- n8n instead of a custom API — pipelines stay visible for demos and iteration.
- One database for records and meaning — no separate vector store.
- On-demand recommendations — expensive text is generated only when HR asks.
- Dynamic apply form — always reflects currently open jobs.
- Split AI roles — Groq for judgment and writing; Mistral for embeddings.



























