Missing Info Microservice
Candidate missing-information outreach, interview-question capture, reply detection, and database update flow.
The candidate missing-info mailer is a FastAPI service that sends missing-information requests and optional interview questions to candidates, watches the sender mailbox for replies, extracts the candidate's answers with LLM passes, and writes the resolved fields back to Talentino's Postgres schema.
It is currently deployed on Google Cloud Run and integrated with:
- Postgres for candidates, resumes, communication activities, templates, auth users, and Gmail watch state.
- AWS SES for outbound email delivery.
- Google Pub/Sub for asynchronous email sending, Gmail reply notifications, and LLM extraction jobs.
- Gmail API for reply detection on the shared sender mailbox.
- Gemini/BAML for PASS 1 missing-info extraction from email replies.
- LiteLLM/DeepInfra for PASS 2 interview-answer classification.
This service is an internal Talentino backend. The browser/frontend calls it through the configured API key. Pub/Sub webhook routes are not user-callable.
Production behavior
The service accepts POST /missing-info-requests requests from the frontend. A request can target one or more Resumes and contains:
- the email subject and rendered body
- the selected missing fields
- field mappings used later by extraction
- optional interview questions, either catalog questions or ad-hoc recruiter questions
- an optional
job_match_idwhen interview questions are attached
The backend resolves each resume_id to its candidate email from Postgres, creates a communication_activities row, and publishes an email-sending task to Pub/Sub. The actual SES send happens asynchronously through /webhooks/email-sending.
Outbound mail is sent from the configured shared sender:
DEFAULT_FROM_EMAIL=test@talentino.io
SES_FROM_EMAIL=test@talentino.ioThe frontend may pass the connected recruiter's email as from_email, but the backend intentionally ignores it for delivery and reply tracking. Replies are expected to go back to the shared Gmail mailbox.
Request shape
The canonical request body is:
{
"subject": "string (1..200 chars)",
// Recruiter email supplied by the frontend.
// Current production behavior: this is retained for audit/context only.
// Actual delivery and reply tracking use DEFAULT_FROM_EMAIL / SES_FROM_EMAIL.
"from_email": "valid email",
"template_id": 123,
// Required when any requests[*].interview_questions is non-empty.
// Optional for missing-info-only emails.
"job_match_id": 1716,
"missing_info_mappings": [
{ "value": "address", "label": "Address" },
{ "value": "availability_date", "label": "Availability Date" },
{ "value": "contract_type_preferences", "label": "Contract Type" },
{ "value": "company_type_preferences", "label": "Company Type" },
{ "value": "work_mode_preferences", "label": "Work Mode" }
],
"requests": [
{
"resume_id": 5627,
"requested_info": [
"address",
"availability_date",
"contract_type_preferences"
],
"body": "Rendered email body, including missing-info text and optional interview questions.",
"interview_questions": [
// Catalog question: recruiter selected an existing public.interview_questions row.
{ "interview_question_id": 45, "display_order": 0 },
// Ad-hoc question: recruiter typed it directly in the modal.
{ "question_text": "How are you doing this week?", "display_order": 1 }
]
}
]
}Important validation rules:
requested_infomust be a subset ofmissing_info_mappings[].value.- Each
interview_questions[]item must provide exactly one ofinterview_question_idorquestion_text. job_match_idis required when at least one interview question is attached because promoted ad-hoc questions are inserted intointerview_questions, wherejob_match_idis required.- Catalog question ids must exist in
public.interview_questions; the backend resolves the canonical question text at send time.
End-to-end flow
- A recruiter selects missing fields in the Talentino UI.
- If the Resume is linked to a Job Match, the frontend can also attach interview questions:
- catalog questions from
public.interview_questions - ad-hoc questions typed directly by the recruiter
- catalog questions from
- The frontend renders the email body from the selected template and calls
POST /missing-info-requests. - The backend verifies the shared sender user, ensures a Gmail watch exists, checks SES verification, creates
communication_activities, and publishes one Pub/Sub email task per Resume. - If interview questions are attached, the backend also inserts rows into
communication_activity_interview_questionsto preserve question text, source, and display order. - Pub/Sub pushes the email task to
/webhooks/email-sending. - The service sends the email through SES and stores the SES
message_idincommunication_activities.method_data. - The candidate replies to
test@talentino.io. - Gmail sends a notification through Pub/Sub to
/webhooks/email-reply. - The service fetches Gmail history, matches the reply to the original
message_id, and publishes an LLM extraction task. - Pub/Sub pushes the extraction task to
/webhooks/llm-extraction. - PASS 1 extracts only the requested missing-info fields; the mapper updates the candidate/resume tables and records modification history where supported.
- PASS 2 classifies interview-question answers; only scoring-relevant answers are persisted.
Interview questions pipeline
Interview-question handling is part of the same reply-processing pipeline, but it is intentionally separated from missing-info extraction.
At send time:
- Catalog questions are sent as
{ interview_question_id, display_order }. - Ad-hoc questions are sent as
{ question_text, display_order }. - The backend writes both kinds into
communication_activity_interview_questions. - For catalog questions,
question_textis copied frominterview_questions.questionso the later classifier does not depend on the catalog row still being unchanged. - For ad-hoc questions,
interview_question_idstarts asNULLandis_ad_hoc=true.
At reply-processing time:
- PASS 1 uses BAML/Gemini to extract missing CV/profile information.
- PASS 2 uses LiteLLM with the configured classifier model, currently via DeepInfra, to classify answers to the interview questions attached to the activity.
- The classifier receives the raw candidate reply plus the ordered question list from
communication_activity_interview_questions. - It returns one classification per question:
join_id, extracted verbatimanswer, andis_scoring_relevant. - If the answer is missing, vague, small talk, or off-topic, it is not persisted.
- If a catalog question has a scoring-relevant answer, the service updates
interview_questions.response. - If an ad-hoc question has a scoring-relevant answer, the service inserts a new
interview_questionsrow withsource='custom', then links the join row to the new question id.
PASS 2 failures are non-fatal. If missing-info extraction succeeds but the classifier fails or times out, the service keeps the PASS 1 updates and logs/skips interview Q&A persistence.
Auth and access
Normal API routes use:
X-API-Key: <CANDIDATE_MISSING_INFO_EMAILER_API_KEY>The auth middleware exempts:
/health/docs/redoc/openapi.json/webhooks/*
Webhook endpoints are protected separately with Pub/Sub push OIDC verification when PUBSUB_PUSH_AUDIENCE is set. In production this value is required.
Data dependencies
The service expects the production auth tables to use the plural Better Auth schema:
| Table | Important columns |
|---|---|
users | id, email, name, email_verified, created_at, updated_at |
sessions | id, token, user_id, expires_at, ip_address, user_agent, created_at, updated_at |
accounts | id, account_id, provider_id, user_id, access_token, refresh_token, access_token_expires_at, refresh_token_expires_at, created_at, updated_at |
verifications | id, identifier, value, expires_at, created_at, updated_at |
For the shared sender flow, test@talentino.io must exist in users, must have a Google accounts row with Gmail OAuth tokens, and must be individually verified in SES.
Reply tracking also requires gmail_mailbox_watches.account_id to reference accounts.id.
Business data written by extraction
The extraction mapper updates the production Resume schema. Common writes include:
| Candidate answer | Target |
|---|---|
| phone number | candidates.phone_number |
| address | resume_locations.address |
| availability status/date, notice period, relocation, salary, visa, career objectives | resume_preferences |
| contract type preferences | resume_contract_type_preferences.contract_type |
| company type preferences | resume_company_type_preferences.company_type |
| work mode preferences | resume_work_mode_preferences.work_mode |
| location preferences | resume_location_preferences |
| interview question answers | interview_questions.response |
When interview questions are included, catalog questions are linked through communication_activity_interview_questions. Ad-hoc questions are stored there first and can be promoted into interview_questions during reply processing.
Templates
The frontend template dropdown is loaded from Postgres, not from this service. The expected production template setup is:
communication_reasons.name = 'missing_resume_info'- an
email_templatesrow associated with that reason - the template scoped to the correct
business_unit_id
The template body should keep missing information and interview questions visually separated. The backend does not choose the template; it sends the rendered subject/body provided by the frontend.
Operational checks
Useful Cloud Run log checks:
gcloud run services logs read talentino-candidate-missing-info-emailer \
--region=us-central1 \
--project=dev-talentino \
--limit=300Look for these milestones:
Published message ... to projects/.../topics/email-sending
Processing email send ...
Email sent successfully to ...
Processing Gmail notification ...
Successfully processed LLM extraction ...If the frontend reports 0 queued / 1 failed, inspect the API response backend.results[0].error. Common causes:
| Error | Meaning |
|---|---|
resume_not_found_or_missing_email | The Cloud Run database cannot resolve resume_id to a candidate with a non-null email. Usually the wrong DATABASE_URL secret/version is deployed. |
invalid_requested_info | A requested field value is not present in missing_info_mappings. |
pubsub_publishing_failed | The activity was created, but publishing to the email-sending topic failed. |
shared_sender_user_not_found | test@talentino.io is missing from users in the deployed database. |
missing_google_tokens | The shared sender user has no usable Google OAuth tokens in accounts. |
sender_email_not_verified | The sender email is not individually verified in SES. |
Pub/Sub topology
Production uses push subscriptions:
| Topic | Push endpoint | Purpose |
|---|---|---|
email-sending | /webhooks/email-sending | Send queued emails through SES. |
email-reply | /webhooks/email-reply | Receive Gmail watch notifications and find replies. |
email-processing | /webhooks/llm-extraction | Extract reply data and update Postgres. |
ENABLE_PULL_SUBSCRIBERS=false should be used for Cloud Run webhook mode.
API reference
See the Missing Info API reference for request/response schemas.