Agent Playbook, build tutorial

How to Build an AI Agent for Recruitment Screening

A walkthrough of how to build a screening agent that helps you read a pile of applications fairly: it extracts a job-relevant profile with protected attributes redacted, scores each candidate against an explicit rubric with a written reason per criterion, and ranks them, while a person makes every decision. The agent recommends; it never auto-rejects or auto-hires. Every code step is shown in three frameworks, Google ADK, the Claude SDK, and LangGraph, so you can build it on the stack your team already knows. The code here is the agent's core logic; standing it up in production also means adding the setup, integrations, and hardening around it.

Why screening needs to be auditable and fair

A single open role can draw hundreds of applications, so screening by hand is slow and uneven: the bar drifts with fatigue, different screeners weigh things differently, and the name, photo, and age sitting at the top of a resume can sway a call before the experience is even read. The result is time lost and decisions that are hard to explain later.

A screening agent does the first pass under a fixed rubric. It redacts protected attributes, scores every candidate against the same job-relevant criteria with a reason for each, and hands a ranked, explained shortlist to a person who decides. It reduces bias and makes screening consistent and auditable, but it does not remove bias, and it never makes the call itself. For the upstream and adjacent work, this pairs with the document-processing and lead-qualification playbooks.

Pick a framework: ADK, Claude SDK, or LangGraph

You do not build the agent loop from scratch. A framework gives you the moving parts, and you supply the logic: a step is a model plus an instruction plus a typed schema or a few tools. The code tabs on the build steps show the same extract, score, and guardrail flow three ways.

Google ADK composes the steps with a SequentialAgent and a Runner, the Claude SDK drives a tool-use loop, and LangGraph builds an explicit graph. The redaction, the weighted ranking, the bias guardrails, and the human-in-the-loop routing are deterministic and identical in all three; only the orchestration and the model calls differ.

The architecture

Every application flows through one loop: ingested and parsed, profiled with protected attributes redacted, scored against the rubric, then ranked. Bias guardrails run, and the ranked, explained shortlist goes to a human who decides. Nothing is auto-rejected. Guardrails for redaction, audit, and the human-decides rule wrap the whole thing.

1. Ingest and parse ATS or inbox, resume to text 2 to 3. Extract and score redacted profile, rubric + reasons 4. Rank weighted rubric score 5. Bias guardrails proxy + disparate impact 6. Human decides agent recommends only 7. Explain criteria, scores, reasons logged 8. Run and monitor scheduled, audited, fair Guardrails redact protected never auto-reject audit log shortlist per-criterion shown rubric feedback

Before you build: prerequisites

Inputs and access

A role with a written rubric and weights, the applications to screen, and API access to your ATS (applicant tracking system) or a careers inbox.

Systems to connect

A model API key (Gemini, Claude, or both), your ATS, and a review queue where recruiters see the ranked, explained shortlist and decide.

Governance

Which attributes to redact, the rubric and weights, fairness and disparate-impact monitoring, DPDP Act 2023 handling, and who makes the final call.

Setup: install and keys

Install the packages and set your Gemini key. That is enough to run the runnable file below, which redacts, profiles, and scores a sample resume against a rubric locally. For production you also connect, with API credentials, your ATS or careers inbox and the review queue where a person sees the shortlist and decides.

Shell (install and key)
# Python 3.10 or newer
python -m venv venv && source venv/bin/activate

# The runnable path below uses Google ADK + Gemini:
pip install google-adk google-genai pydantic

# Your Gemini key from aistudio.google.com/apikey:
export GEMINI_API_KEY="your-key-here"

The stack, and why each piece

Layer Pick Why
Application intake Your ATS (applicant tracking system) API, or an email and form inbox, resume parsed to text Read each new application from the tracking system your team already uses, or from a careers inbox, and turn the resume PDF or document into clean text the rest of the pipeline can read. The same store works across sources.
Profile extractor A model (Gemini 2.5 Flash, Claude) with a typed schema Pull only the job-relevant fields into a typed profile, skills, years of experience, qualifications, and at the same moment redact protected attributes (name, gender, age, photo) so they never reach scoring. The code tabs show this three ways.
Rubric scorer A model plus deterministic code, one score per criterion with a written reason Score each role criterion against an explicit written rubric and record a reason for every score, so the result is explainable per criterion, not a single black-box number.
Ranker Deterministic: a weighted sum of the rubric scores Combine the per-criterion scores into one ranking using the weights you set for the role. Plain arithmetic, testable and auditable, not a model guess.
Bias guardrails Deterministic: redaction plus proxy and disparate-impact checks Strip protected attributes before scoring, then audit for proxy signals and for disparate impact across groups. These reduce bias; they do not remove it, and human oversight stays essential.
Human review and audit A review queue, plus an immutable audit log Every shortlist goes to a person who decides; the agent only recommends. The log holds each candidate's criteria, scores, reasons, and what the reviewer did, so any decision can be explained later.

Build it, step by step

The extract, score, and assemble steps show the same logic in Google ADK, the Claude SDK, and LangGraph. Use the tabs to switch. The intake, ranking, guardrail, and human-routing steps are deterministic Python and identical everywhere. These are the building blocks, and they call your own systems (the ATS, the review queue) by name; the complete file you can run today is in "Put it together" below.

1

Ingest applications and parse each resume to text

Start with the connection, not the cleverness. The agent reads each new application through your ATS (applicant tracking system) API or from a careers inbox, and turns the resume PDF or document into clean text. It keys each one on the application id, which makes the pipeline idempotent: an application is screened once, even if the job re-runs or a webhook fires twice. Deterministic, and the same in every framework.

Python (intake + parse)
import fitz  # PyMuPDF, for resume PDFs

def pull_new(ats, seen: set) -> list[dict]:
    out = []
    for app in ats.list_applications(status="new"):
        if app["id"] in seen:                 # already screened
            continue
        seen.add(app["id"])
        text = parse_resume(app["resume_url"])    # PDF / docx -> plain text
        out.append({"id": app["id"], "role": app["role_id"], "text": text})
    return out

def parse_resume(url: str) -> str:
    doc = fitz.open(stream=download(url), filetype="pdf")
    return "\n".join(page.get_text() for page in doc)
# Application id makes it idempotent: each candidate is screened once, even on a re-run.
2

Extract a job-relevant profile and redact protected attributes

Before anything is scored, the agent pulls only the job-relevant fields into a typed profile, skills, years of experience, qualifications, and at the same moment redacts protected attributes so they never reach scoring: name, gender, age, photo, religion, and marital status. Because the schema is typed, the output is structured data, and because protected fields are dropped here rather than later, the scorer never sees them. This reduces bias; it does not remove it. The tabs show the same redacting extraction three ways.

from google import genai
from google.genai import types
from pydantic import BaseModel
client = genai.Client()

class Profile(BaseModel):
    skills: list[str]; years_experience: float
    qualifications: list[str]; relevant_titles: list[str]
    # Note: NO name, gender, age, photo, religion, or marital status.

def extract_profile(resume_text: str) -> Profile:
    res = client.models.generate_content(
        model="gemini-2.5-flash",
        contents=["Extract only the job-relevant fields below. Do NOT include "
                  "name, gender, age, photo, religion, or marital status.\n" + resume_text],
        config=types.GenerateContentConfig(
            response_mime_type="application/json", response_schema=Profile))
    return res.parsed                # a typed Profile, protected fields excluded
from anthropic import Anthropic
client = Anthropic()
# Schema deliberately omits name, gender, age, photo, religion, marital status.
SCHEMA = {"type": "object", "properties": {
    "skills": {"type": "array", "items": {"type": "string"}},
    "years_experience": {"type": "number"},
    "qualifications": {"type": "array", "items": {"type": "string"}},
    "relevant_titles": {"type": "array", "items": {"type": "string"}}},
    "required": ["skills", "years_experience", "qualifications"]}

def extract_profile(resume_text: str) -> dict:
    res = client.messages.create(model="claude-opus-4-8", max_tokens=1024,
        tools=[{"name": "save_profile", "input_schema": SCHEMA}],
        tool_choice={"type": "tool", "name": "save_profile"},
        messages=[{"role": "user", "content":
            "Extract only the job-relevant fields. Do not include name, gender, "
            "age, photo, religion, or marital status.\n" + resume_text}])
    return res.content[0].input
from langchain_google_genai import ChatGoogleGenerativeAI
from pydantic import BaseModel

class Profile(BaseModel):
    skills: list[str]; years_experience: float
    qualifications: list[str]; relevant_titles: list[str]
    # No name, gender, age, photo, religion, or marital status.

extractor = ChatGoogleGenerativeAI(
    model="gemini-2.5-flash").with_structured_output(Profile)

def extract_profile(resume_text: str) -> Profile:
    return extractor.invoke(
        "Extract only the job-relevant fields. Do not include name, gender, "
        "age, photo, religion, or marital status.\n" + resume_text)
3

Score each role criterion against an explicit rubric, with a reason

This is where the agent earns its keep, and where it stays honest. For each criterion in the role's written rubric, the model returns a score and a written reason for that score, judging the redacted profile against the role only. The output is per-criterion and explainable, not one opaque overall number. The reasons are what a reviewer reads and what the audit log keeps. The tabs show the same scoring three ways.

from google import genai
from google.genai import types
from pydantic import BaseModel
client = genai.Client()

class CriterionScore(BaseModel):
    criterion: str; score: int; reason: str   # 0-5, with a written reason

class Scored(BaseModel):
    scores: list[CriterionScore]

def score(profile: dict, rubric: list[dict]) -> Scored:
    res = client.models.generate_content(
        model="gemini-2.5-flash",
        contents=[f"Score this profile against each rubric criterion 0-5 and give "
                  f"a one-line reason per criterion. Judge job-relevant evidence "
                  f"only.\nRubric: {rubric}\nProfile: {profile}"],
        config=types.GenerateContentConfig(
            response_mime_type="application/json", response_schema=Scored))
    return res.parsed                # per-criterion scores, each with a reason
from anthropic import Anthropic
client = Anthropic()
SCHEMA = {"type": "object", "properties": {
    "scores": {"type": "array", "items": {"type": "object", "properties": {
        "criterion": {"type": "string"}, "score": {"type": "integer"},
        "reason": {"type": "string"}},
        "required": ["criterion", "score", "reason"]}}},
    "required": ["scores"]}

def score(profile: dict, rubric: list) -> dict:
    res = client.messages.create(model="claude-opus-4-8", max_tokens=1024,
        tools=[{"name": "save_scores", "input_schema": SCHEMA}],
        tool_choice={"type": "tool", "name": "save_scores"},
        messages=[{"role": "user", "content":
            f"Score this profile against each rubric criterion 0-5 with a reason. "
            f"Judge job-relevant evidence only.\nRubric: {rubric}\nProfile: {profile}"}])
    return res.content[0].input      # never auto-decides; only scores with reasons
from langchain_google_genai import ChatGoogleGenerativeAI
from pydantic import BaseModel

class CriterionScore(BaseModel):
    criterion: str; score: int; reason: str

class Scored(BaseModel):
    scores: list[CriterionScore]

scorer = ChatGoogleGenerativeAI(
    model="gemini-2.5-flash").with_structured_output(Scored)

def score(profile: dict, rubric: list) -> Scored:
    return scorer.invoke(
        f"Score this profile against each rubric criterion 0-5 with a reason. "
        f"Judge job-relevant evidence only.\nRubric: {rubric}\nProfile: {profile}")
4

Rank candidates by the weighted rubric score

With per-criterion scores in hand, the ranking is plain arithmetic, not another model call. Each criterion carries a weight you set for the role, and the agent computes a weighted sum to order candidates. Because it is deterministic, the same inputs always give the same order, and you can show exactly how a total was reached. This is a recommendation for a person to review, never a decision. Identical in every framework.

Python (deterministic rank)
def total_score(scores: list[dict], weights: dict[str, float]) -> float:
    # weighted sum of per-criterion scores; weights are set per role
    return sum(s["score"] * weights.get(s["criterion"], 1.0) for s in scores)

def rank(candidates: list[dict], weights: dict[str, float]) -> list[dict]:
    for c in candidates:
        c["total"] = total_score(c["scores"], weights)
    return sorted(candidates, key=lambda c: c["total"], reverse=True)
# Plain arithmetic: same inputs, same order, fully explainable. A recommendation,
# not a decision; a person reviews the ranked list.
5

Apply bias guardrails: redact, then audit for proxies and disparate impact

Redacting protected attributes is necessary but not sufficient, because proxies leak. So the agent runs deterministic checks: it confirms no protected field reached the scorer, scans the text for proxy signals (a postcode, a college that stands in for background, a graduation year that reveals age), and measures disparate impact, comparing shortlist rates across groups where that data is held separately and lawfully for monitoring. These reduce bias and surface problems for a human to act on; they do not remove bias. The same code everywhere.

Python (deterministic guardrails)
PROTECTED = {"name", "gender", "age", "photo", "religion", "marital_status"}
PROXY_HINTS = ("pin code", "postcode", "graduated", "date of birth", "dob")

def guardrail_check(profile: dict, raw_text: str) -> list[str]:
    flags = []
    if PROTECTED & set(profile):               # a protected field leaked in
        flags.append("protected_attribute_present")
    if any(h in raw_text.lower() for h in PROXY_HINTS):
        flags.append("possible_proxy_signal")   # for human attention
    return flags

def disparate_impact(shortlisted: dict, applied: dict) -> dict:
    # rates compared across groups, using data held separately for monitoring only
    rates = {g: shortlisted.get(g, 0) / max(applied.get(g, 1), 1) for g in applied}
    top = max(rates.values()) or 1
    # flag any group whose selection rate is below 80% of the highest (a common check)
    return {g: r for g, r in rates.items() if r < 0.8 * top}
# Reduces bias and surfaces issues for review. It does not remove bias; people decide.
6

Route every shortlist to a human: the agent recommends, people decide

This is the rule the whole design exists to protect: the agent never auto-rejects and never auto-hires. It produces a ranked, explained shortlist and hands it to a person, who reads the per-criterion reasons and decides who advances. A candidate flagged by the guardrails, or one the rubric scores near a threshold, is surfaced for closer human attention rather than silently dropped. Deterministic, and the same in every framework.

Python (human-in-the-loop)
def route_for_review(ranked: list[dict], flags: dict[str, list]) -> None:
    for c in ranked:
        review_queue.add(candidate=c["id"], scores=c["scores"],
                         total=c["total"], flags=flags.get(c["id"], []))
    # The agent NEVER auto-rejects or auto-hires. It recommends; a person decides.

def on_human_decision(candidate_id: str, decision: str, reviewer: str) -> None:
    # the only place an outcome is set, and only by a named person
    audit.log("decision", candidate_id, {"decision": decision, "by": reviewer})
# No code path closes out a candidate without a human. Recommendations only.
7

Explain and audit: log per-candidate criteria, scores, and reasons

An explainable decision is one you can reconstruct, so the agent writes everything down. For each candidate it logs the rubric used, the score and the written reason per criterion, the weighted total, any guardrail flags, and the reviewer's decision with their name and time. If a candidate or a regulator later asks why someone was passed over, the answer is in the record, not in someone's memory. Deterministic, and the same in every framework.

Python (audit log)
def log_screening(candidate: dict, rubric: list, flags: list) -> None:
    audit.write({
        "candidate_id": candidate["id"],
        "rubric": rubric,                      # the exact criteria and weights used
        "scores": candidate["scores"],         # per-criterion score + reason
        "total": candidate["total"],
        "guardrail_flags": flags,
        "model": "gemini-2.5-flash",
        "at": now(),
    })   # immutable; the human decision is appended later, with the reviewer's name
# Every score, reason, and flag is recorded, so any shortlist can be explained.
8

Assemble the run, with bias, human-in-the-loop, and audit guardrails

The pieces run on each new batch of applications. ADK composes extract, score, and the guardrail step under a SequentialAgent and a Runner, the Claude SDK runs them in a tool-use loop, and LangGraph builds an explicit extract-score-rank graph. Whichever you pick, the same guardrails apply: the application id makes every run idempotent, protected attributes are redacted before scoring, the agent never auto-rejects or auto-hires, fairness and disparate impact are monitored, every score and reason is written to an immutable audit log, and candidate personal data is handled in line with the DPDP Act 2023, whose core obligations phase in through 2027.

from google.adk.agents import SequentialAgent
from google.adk.runners import Runner
from google.adk.sessions import InMemorySessionService

screen_pipeline = SequentialAgent(name="screen_pipeline",
    sub_agents=[extractor, scorer, guardrail])   # each an LlmAgent with the tools

runner = Runner(agent=screen_pipeline, app_name="screening",
                session_service=InMemorySessionService())

def guard(app: dict) -> str | None:
    if store.seen(app["id"]):       return "already_screened"   # idempotent
    if PROTECTED & set(app.get("profile", {})):
        return "redaction_failed"   # never score with protected attributes present
    return None
# Run on each new batch via cron / Cloud Scheduler. The agent recommends; people decide.
from anthropic import Anthropic
client = Anthropic()
# Note: no reject_tool and no hire_tool exist. Recommendations only.
TOOLS = [extract_tool, score_tool, rank_tool, guardrail_tool]   # JSON schemas

def run_screening(messages: list):
    while True:
        resp = client.messages.create(model="claude-opus-4-8", max_tokens=2048,
                                      tools=TOOLS, messages=messages)
        if resp.stop_reason != "tool_use":
            return resp
        messages.append({"role": "assistant", "content": resp.content})
        for b in resp.content:
            if b.type == "tool_use":
                out = dispatch(b.name, b.input)   # no auto-reject / auto-hire tool
                messages.append({"role": "user", "content": [
                    {"type": "tool_result", "tool_use_id": b.id, "content": str(out)}]})
from langgraph.graph import StateGraph, START, END

g = StateGraph(dict)
g.add_node("extract",   lambda s: {"profile": extract_profile(s["text"])})
g.add_node("score",     lambda s: {"scores": score(s["profile"], s["rubric"])})
g.add_node("guardrail", lambda s: {"flags": guardrail_check(s["profile"], s["text"])})
g.add_edge(START, "extract")
g.add_edge("extract", "score")
g.add_edge("score", "guardrail")
g.add_edge("guardrail", END)
screen_pipeline = g.compile()      # invoke per batch; output is a shortlist for a human

Put it together: run it end to end

Here is a runnable file. It redacts protected attributes from a sample resume, extracts a job-relevant profile, and scores it against a small rubric with a reason per criterion, and it does not decide anything. The sample is inline text; production reads the ATS, as in the build steps. Save it as main.py and run python main.py.

Python (full runnable file, ADK)
# main.py  --  run:  python main.py
# A runnable reference: redacts protected attributes from a sample resume,
# extracts a job-relevant profile, and scores it against a small rubric with a
# reason per criterion. It does NOT decide anything: it recommends, a person
# decides. The sample is inline text; production reads the ATS, as in the steps.
import asyncio
from google import genai
from google.genai import types
from google.adk.agents import LlmAgent
from google.adk.runners import InMemoryRunner

client = genai.Client()                       # reads GEMINI_API_KEY

# ---- a sample resume (replace with a real one from your ATS) ----
SAMPLE_RESUME = """Priya Sharma, 29, female. Pune.
Backend engineer, 5 years. Python, Django, PostgreSQL, AWS. Built and ran a
payments service handling 2M requests a day. B.Tech, Computer Science, 2017."""
# -----------------------------------------------------------------

# The written rubric for the role (criteria you set before screening).
RUBRIC = ["Python and backend depth", "Years of relevant experience",
          "Cloud and database skills", "Evidence of scale or ownership"]

def redact_and_extract(resume_text: str) -> str:
    """Drop protected attributes and return only job-relevant fields, as JSON."""
    return client.models.generate_content(
        model="gemini-2.5-flash",
        contents=["Return JSON with keys skills, years_experience, qualifications, "
                  "relevant_titles. Do NOT include name, gender, age, photo, "
                  "religion, or marital status.\n" + resume_text]).text

def score_against_rubric(profile_json: str) -> str:
    """Score the redacted profile per rubric criterion, with a reason each."""
    return client.models.generate_content(
        model="gemini-2.5-flash",
        contents=[f"Score this profile 0-5 against each criterion and give a "
                  f"one-line reason per criterion. Job-relevant evidence only.\n"
                  f"Rubric: {RUBRIC}\nProfile: {profile_json}"]).text

agent = LlmAgent(
    name="screening_agent", model="gemini-2.5-flash",
    instruction=("Call redact_and_extract on the resume, then score_against_rubric "
                 "on the result. Report the redacted profile and the per-criterion "
                 "scores with reasons. State clearly this is a recommendation for a "
                 "human reviewer, and that the agent does not reject or hire anyone."),
    tools=[redact_and_extract, score_against_rubric])

async def main():
    runner = InMemoryRunner(agent=agent, app_name="screening")
    session = await runner.session_service.create_session(app_name="screening", user_id="demo")
    msg = types.Content(role="user",
                        parts=[types.Part(text=f"Screen this resume:\n{SAMPLE_RESUME}")])
    async for event in runner.run_async(user_id="demo", session_id=session.id, new_message=msg):
        if event.is_final_response():
            print(event.content.parts[0].text)

if __name__ == "__main__":
    asyncio.run(main())

The agent reports the redacted profile (skills, years, qualifications, with the name, age, and gender dropped) and a score with a reason for each rubric criterion, framed as a recommendation for a human reviewer. The exact wording is model-generated, so it varies run to run; the agent never rejects or hires anyone.

The review console

A recruiter sees one screen: candidates ranked by rubric score, the redacted profile, the per-criterion scores and reasons, and the decision, which a person always makes. The agent recommends; it never rejects on its own.

Screening review RANKED SHORTLIST Candidate A-114score 4.4 / 5 Candidate A-097score 3.9 / 5 Candidate A-1313.6, proxy flag REDACTED PROFILE Nameredacted Age, genderredacted Python, Django, AWS5 yrs backend; ran apayments service at scale. QualificationsB.Tech CS SCORES + REASONS Python depth 5 strongExperience 4 5 yrsCloud / DB 4 AWS, PGScale 5 2M req/day Recommendation only. You decide. Advance Pass

Build, buy, or no-code

Approach Best for Effort Cost Control
Custom agent (build in-house) You want your own rubric, redaction and audit rules, ATS wiring, and the candidate data in-house High Build cost + API and model usage Full
Screening SaaS (a scrutinised category, vendors vary widely) A fast start with standard scoring, if you accept their model and audit it yourself for fairness Low Subscription per seat or per role Medium
No-code (a keyword filter in your ATS or a spreadsheet) Very low volume, simple must-have keywords, a quick first pass Low Cheap, but crude and easy to game Low

Fairness, explainability, and the law

Recruitment AI is scrutinised and regulated, so this is decision support, not an automated decision. The agent recommends; a human makes every call, and there is no code path that rejects or hires a candidate on its own. Bias is mitigated by redacting protected attributes (name, gender, age, photo, religion, marital status) before scoring and by auditing for proxy signals and disparate impact, which reduces but does not remove bias. Scoring is explainable: per criterion, against a written rubric, with a reason for each.

Every shortlist carries a full audit trail, the rubric, the scores, the reasons, the guardrail flags, and the reviewer's decision, so any outcome can be explained later. Candidate personal data is handled in line with the DPDP Act 2023, whose core obligations phase in through 2027, and you can run the pipeline in your own cloud with access scoped to the roles it screens. Human oversight is required here, and the design keeps it required.

The numbers

These are directional, not promises, and depend on your hiring volume and the roles you screen. The point is the shape of the change: less time reading resumes, more consistent screening, lower bias exposure, and decisions you can explain.

Metric Manual Agent
Time spent reading resumes Hours per role screening every applicant by hand, often only the first page Every application read in full and scored against the rubric, so reviewers start from a ranked, explained shortlist
Consistency of screening Different screeners weigh things differently, and fatigue late in a pile changes the bar The same rubric and weights applied to every candidate, with a reason logged for each score
Bias exposure Name, gender, age, and photo are visible while screening and can sway a call Protected attributes redacted before scoring, with a proxy and disparate-impact audit, which reduces but does not remove bias
Explainability of a decision Hard to reconstruct why a candidate was passed over Per-criterion scores and reasons are logged, so any shortlist can be explained and reviewed

Time and consistency gains depend on hiring volume and role mix. The agent reduces bias exposure but does not remove bias, and a human makes every decision. Figures are ranges, not guarantees.

What makes it fail

  • !Letting it auto-reject or auto-hire, so a candidate is closed out with no human check.
  • !Scoring before redaction, so name, gender, or age can sway the score.
  • !Treating an overall number as the answer, with no per-criterion reasons behind it.
  • !Skipping the proxy and disparate-impact audit, so bias leaks back in through proxies.
  • !Scoring on criteria that are not job-relevant, which is neither fair nor defensible.

A safe rollout

  1. Start in shadow mode on one role: score and rank, but recruiters screen as usual and compare.
  2. Turn on the ranked, explained shortlist as an aid, with the human making every decision.
  3. Add the proxy and disparate-impact monitoring and review the flags each cycle.
  4. Expand to more roles, with a fairness and consistency dashboard and regular rubric review.

FAQs

General FAQs

Everything you need to know about the service and how it works. Can’t find an answer? Mail us at info@galific.com

  • Does the agent auto-reject or auto-hire candidates?
    No, and this is the most important design choice. The agent extracts a job-relevant profile, scores it against an explicit rubric, and ranks candidates, but it never closes out an applicant on its own. It produces a ranked, explained shortlist, and a person reads the per-criterion reasons and decides who advances. There is deliberately no reject or hire action in the code. It is decision support; a human makes every call.
  • How does it reduce bias, and can it eliminate bias?
    It reduces bias; it does not eliminate it, and we are careful to say so. The mechanisms are concrete: protected attributes (name, gender, age, photo, religion, marital status) are redacted before any scoring, the model scores only job-relevant criteria from a written rubric, and deterministic checks audit for proxy signals and for disparate impact across groups. These lower the chance that irrelevant attributes sway a score, but no system removes bias entirely, which is exactly why human oversight stays essential.
  • Are the scores explainable, or is it a black-box number?
    Every score is per criterion against the written rubric, with a one-line reason for each, not a single opaque overall figure. A reviewer sees, for example, why a candidate scored well on a required skill and poorly on years of experience, in plain language. The ranking is then a deterministic weighted sum of those scores, so you can show exactly how a total was reached. Explainability is the point, not an add-on.
  • What is the rubric, and who sets it?
    The rubric is the written list of criteria for the role, each with a weight, that you define before screening begins: required skills, relevant experience, qualifications, and so on. The agent scores every candidate against the same rubric, which is what makes screening consistent and auditable. Because the rubric is explicit and stored with each decision, you can review it, change it, and show what it was when a given candidate was scored.
  • Does it integrate with my ATS (applicant tracking system)?
    Yes, through your ATS API. The agent reads new applications from the tracking system your team already uses, parses the resume to text, and writes the scores, reasons, and shortlist back, so recruiters work in the tool they know. Where an application arrives by email or a careers form instead, the same pipeline reads it from there. The candidate store and audit log are the same regardless of source.
  • Recruitment AI is heavily scrutinised. How does this stay on the right side of the law?
    Recruitment AI is scrutinised and regulated, and we treat that as a starting assumption rather than an afterthought. The agent is decision support, not an automated decision: a human makes every call, protected attributes are redacted before scoring, fairness and disparate impact are monitored, and a full audit trail records the rubric, scores, and reasons behind every shortlist. Candidate personal data is handled in line with the DPDP Act 2023, whose core obligations phase in through 2027. Human oversight is required, and the design keeps it required.
  • Is candidate data kept private and secure?
    Yes. Resumes are sensitive personal data, so they are handled in line with the DPDP Act 2023, whose core obligations phase in through 2027, and you can run the pipeline in your own cloud with access scoped to the roles it screens. The scorer sees only the redacted, job-relevant profile, not the protected attributes, and every action is written to an audit log. Any group data used for disparate-impact monitoring is held separately and used for monitoring only.
  • Should I build a custom agent or buy a screening SaaS?
    Buy a screening SaaS for a fast start with standard scoring, provided you accept its model and audit it yourself for fairness, because the category is scrutinised and vendors vary widely in how they handle bias and explainability. Build, or have it built, when you need your own rubric, your own redaction and audit rules, tight wiring into your ATS, and the candidate data in-house. Whichever you choose, the human-decides rule and the audit trail are non-negotiable. See the comparison table above.
  • Can it screen resumes written in Indian languages?
    The models read resumes in English and major Indian languages, so an application in Hindi or a bilingual resume can be parsed, profiled, and scored against the same rubric. As always, quality varies with the language and the writing, and the human review step means a profile the model read poorly is caught by a person rather than acted on automatically. When in doubt, the candidate is surfaced for closer attention, not dropped.
  • What should this agent never do?
    It should never auto-reject or auto-hire, never score using protected attributes, never present an overall number without the per-criterion reasons behind it, and never be the final word on a candidate. It also should not be pointed at criteria that are not job-relevant, or used to screen out groups, which is what the disparate-impact monitoring exists to catch. Anything that removes the human or hides the reasoning is outside what this design allows.
  • How accurate is the scoring, and what if the model gets it wrong?
    Scoring is a strong, consistent first pass against the rubric, not a verdict, which is why a person reviews every shortlist and the reasons behind each score. If the model misreads a resume, the written reason usually makes that visible to the reviewer, who corrects it; and because ranking is a transparent weighted sum, an odd total can be traced to the score that caused it. The agent narrows the pile and explains itself; people make the decision.
  • What does it cost to run, and how fast is payback?
    Running cost is mostly model and API usage plus hosting, which is small against the hours a team spends reading resumes for every open role. Because each application is read in full and scored consistently, the saving shows up immediately in faster, more consistent shortlisting and in a defensible record of how each decision was made, so for a team hiring at any volume the build tends to pay for itself quickly.

Want this built and running in your stack?

Galific designs, builds, and runs screening agents like this on ADK, the Claude SDK, or LangGraph, wired into your ATS, with protected attributes redacted, scoring explainable per criterion, and a person making every decision. Or explore the ready-made versions in our agent suite.