Talentino Docs

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_id when 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.io

The 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_info must be a subset of missing_info_mappings[].value.
  • Each interview_questions[] item must provide exactly one of interview_question_id or question_text.
  • job_match_id is required when at least one interview question is attached because promoted ad-hoc questions are inserted into interview_questions, where job_match_id is required.
  • Catalog question ids must exist in public.interview_questions; the backend resolves the canonical question text at send time.

End-to-end flow

  1. A recruiter selects missing fields in the Talentino UI.
  2. 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
  3. The frontend renders the email body from the selected template and calls POST /missing-info-requests.
  4. 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.
  5. If interview questions are attached, the backend also inserts rows into communication_activity_interview_questions to preserve question text, source, and display order.
  6. Pub/Sub pushes the email task to /webhooks/email-sending.
  7. The service sends the email through SES and stores the SES message_id in communication_activities.method_data.
  8. The candidate replies to test@talentino.io.
  9. Gmail sends a notification through Pub/Sub to /webhooks/email-reply.
  10. The service fetches Gmail history, matches the reply to the original message_id, and publishes an LLM extraction task.
  11. Pub/Sub pushes the extraction task to /webhooks/llm-extraction.
  12. PASS 1 extracts only the requested missing-info fields; the mapper updates the candidate/resume tables and records modification history where supported.
  13. 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_text is copied from interview_questions.question so the later classifier does not depend on the catalog row still being unchanged.
  • For ad-hoc questions, interview_question_id starts as NULL and is_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 verbatim answer, and is_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_questions row with source='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:

TableImportant columns
usersid, email, name, email_verified, created_at, updated_at
sessionsid, token, user_id, expires_at, ip_address, user_agent, created_at, updated_at
accountsid, account_id, provider_id, user_id, access_token, refresh_token, access_token_expires_at, refresh_token_expires_at, created_at, updated_at
verificationsid, 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 answerTarget
phone numbercandidates.phone_number
addressresume_locations.address
availability status/date, notice period, relocation, salary, visa, career objectivesresume_preferences
contract type preferencesresume_contract_type_preferences.contract_type
company type preferencesresume_company_type_preferences.company_type
work mode preferencesresume_work_mode_preferences.work_mode
location preferencesresume_location_preferences
interview question answersinterview_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_templates row 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=300

Look 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:

ErrorMeaning
resume_not_found_or_missing_emailThe 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_infoA requested field value is not present in missing_info_mappings.
pubsub_publishing_failedThe activity was created, but publishing to the email-sending topic failed.
shared_sender_user_not_foundtest@talentino.io is missing from users in the deployed database.
missing_google_tokensThe shared sender user has no usable Google OAuth tokens in accounts.
sender_email_not_verifiedThe sender email is not individually verified in SES.

Pub/Sub topology

Production uses push subscriptions:

TopicPush endpointPurpose
email-sending/webhooks/email-sendingSend queued emails through SES.
email-reply/webhooks/email-replyReceive Gmail watch notifications and find replies.
email-processing/webhooks/llm-extractionExtract 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.

On this page