InterviewKata
AI engineering · Case study 13 · AI in the real world

LinkedIn: Hiring Assistant

Hiring Assistant helps recruiters with the slow parts of hiring: turning a role into qualifications, finding candidates, assessing them against those qualifications, and drafting outreach. In October 2025 LinkedIn's engineers described how they built it, their first production agent. It combines most of the architectures in Module 07a: a supervisor that plans and then executes, specialised sub-agents, and humans approving every decision they're accountable for. It shows what those choices look like when they have to hold up in an enterprise product.

A 2-minute conversation: Sam asks, Priya explains
0:00 / 2:15

1. The situation

Recruiting is a long, multi-step workflow with real consequences for people. A wrong qualification filters out good candidates; a wrong message reaches a real person; a decision about a candidate is one a recruiter must be able to explain. An agent here has to be thorough, consistent and controllable, not just capable.

Real case, from the buildersAll facts and quotes are from "How we engineered LinkedIn's Hiring Assistant" (Xiaoyang Gu, with Xie Lu and Daniel Hewlett, LinkedIn Engineering, 21 October 2025). The candidates and skills in the demo are invented.

2. What they chose, and why

ChoiceFrom the post
Plan-and-executeThey chose it because "ReAct alone is not sufficient for an enterprise-grade agent". A planner writes a structured plan, and an executor carries out each step with a ReAct-style loop; the planner must "explicitly think through all steps".
A supervisor"The 'central nervous system' of Hiring Assistant": it plans, interprets the recruiter's input, and delegates to tools or sub-agents. All user and agent interactions flow through it.
Specialised sub-agentsIntake, sourcing, evaluation, candidate outreach, candidate screening, learning and cognitive memory. Some are modelled as tools rather than separate agents.
Humans in the loopThe goal is to "ensure that humans are in the loop for all decisions that humans are accountable for". Recruiters decide who advances; changes the learning agent proposes are "applied only after their review and approval".

3. How a hiring request flows

4. The flow in code

A tiny version: the supervisor plans six steps, then executes them. The recruiter approves the qualifications, the evaluation shows its evidence for each candidate, and the recruiter decides who advances, dropping one shortlisted candidate for a reason the agent can't know. The learning step then draws the wrong lesson ("only AWS users were advanced") and proposes a change, which waits for the recruiter instead of being applied. That's why learned changes need approval. The program was run in all three languages; each prints the output below it.

# Invented candidates: name -> skills.
CANDIDATES = {
    "Asha": {"python", "kafka", "aws"}, "Ben": {"python", "aws"}, "Chen": {"java", "kafka"},
    "Dara": {"python", "kafka", "gcp"}, "Eli": {"go"}, "Fatima": {"python", "kafka", "aws", "terraform"},
}
log = []


def recruiter(question, proposal):
    """The human checkpoint. Here the recruiter's answers are scripted; in production the run waits for them."""
    answers = {"approve qualifications": proposal,
               "advance which candidates": [c for c in proposal if c != "Dara"],     # recruiter knows Dara just accepted another offer
               "apply learned change": None}                                          # not reviewed yet
    answer = answers[question]
    shown = "pending" if answer is None else ", ".join(answer) if isinstance(answer, list) else answer
    log.append(f"  [human] {question}: {shown}")
    return answer


def intake(request):
    must = {"python", "kafka"}
    return recruiter("approve qualifications", sorted(must))


def sourcing(must):
    return [c for c, skills in CANDIDATES.items() if skills & set(must)]


def evaluation(found, must):
    scored = [(c, len(CANDIDATES[c] & set(must)), sorted(CANDIDATES[c] & set(must))) for c in found]
    for c, score, evidence in scored:
        log.append(f"  evaluation: {c} matches {score}/{len(must)} ({', '.join(evidence)})")
    return [c for c, score, _ in scored if score == len(must)]


def plan(request):
    """The supervisor plans every step up front, then executes them in order (plan-and-execute)."""
    return ["intake", "sourcing", "evaluation", "review", "outreach", "learning"]


state = {}
for step in plan("Senior backend engineer, streaming"):
    log.append(f"step: {step}")
    if step == "intake":
        state["must"] = intake("Senior backend engineer, streaming")
    elif step == "sourcing":
        state["found"] = sourcing(state["must"])
        log.append(f"  sourcing: {len(state['found'])} candidates found")
    elif step == "evaluation":
        state["shortlist"] = evaluation(state["found"], state["must"])
    elif step == "review":
        state["advance"] = recruiter("advance which candidates", state["shortlist"])
    elif step == "outreach":
        log.append(f"  outreach: messages drafted for {', '.join(state['advance'])}")
    elif step == "learning":
        proposal = "add 'aws' as a preferred skill"
        log.append(f"  learning: recruiter advanced only AWS users -> proposes: {proposal}")
        if recruiter("apply learned change", proposal) is None:
            log.append("  learning: change held until the recruiter approves it")
print("\n".join(log))
// Invented candidates: name -> skills.
const CANDIDATES = new Map<string, string[]>([
  ["Asha", ["python", "kafka", "aws"]], ["Ben", ["python", "aws"]], ["Chen", ["java", "kafka"]],
  ["Dara", ["python", "kafka", "gcp"]], ["Eli", ["go"]], ["Fatima", ["python", "kafka", "aws", "terraform"]],
]);
const log: string[] = [];

// The human checkpoint. Here the recruiter's answers are scripted; in production the run waits for them.
function recruiter(question: string, proposal: string[] | string): string[] | string | null {
  let answer: string[] | string | null = null; // "apply learned change": not reviewed yet
  if (question === "approve qualifications") answer = proposal;
  if (question === "advance which candidates") answer = (proposal as string[]).filter((c) => c !== "Dara"); // recruiter knows Dara just accepted another offer
  const shown = answer === null ? "pending" : Array.isArray(answer) ? answer.join(", ") : answer;
  log.push(`  [human] ${question}: ${shown}`);
  return answer;
}

const intake = (request: string) => recruiter("approve qualifications", ["kafka", "python"]) as string[];
const sourcing = (must: string[]) => [...CANDIDATES].filter(([, skills]) => skills.some((s) => must.includes(s))).map(([c]) => c);

function evaluation(found: string[], must: string[]): string[] {
  const shortlist: string[] = [];
  for (const c of found) {
    const evidence = CANDIDATES.get(c)!.filter((s) => must.includes(s)).sort();
    log.push(`  evaluation: ${c} matches ${evidence.length}/${must.length} (${evidence.join(", ")})`);
    if (evidence.length === must.length) shortlist.push(c);
  }
  return shortlist;
}

// The supervisor plans every step up front, then executes them in order (plan-and-execute).
const plan = (request: string) => ["intake", "sourcing", "evaluation", "review", "outreach", "learning"];

let must: string[] = [], found: string[] = [], shortlist: string[] = [], advance: string[] = [];
for (const step of plan("Senior backend engineer, streaming")) {
  log.push(`step: ${step}`);
  if (step === "intake") must = intake("Senior backend engineer, streaming");
  else if (step === "sourcing") { found = sourcing(must); log.push(`  sourcing: ${found.length} candidates found`); }
  else if (step === "evaluation") shortlist = evaluation(found, must);
  else if (step === "review") advance = recruiter("advance which candidates", shortlist) as string[];
  else if (step === "outreach") log.push(`  outreach: messages drafted for ${advance.join(", ")}`);
  else if (step === "learning") {
    const proposal = "add 'aws' as a preferred skill";
    log.push(`  learning: recruiter advanced only AWS users -> proposes: ${proposal}`);
    if (recruiter("apply learned change", proposal) === null) log.push("  learning: change held until the recruiter approves it");
  }
}
console.log(log.join("\n"));
import java.util.*;
import java.util.stream.*;

public class Main {
    // Invented candidates: name -> skills.
    static final Map<String, Set<String>> CANDIDATES = new LinkedHashMap<>();
    static {
        CANDIDATES.put("Asha", Set.of("python", "kafka", "aws")); CANDIDATES.put("Ben", Set.of("python", "aws"));
        CANDIDATES.put("Chen", Set.of("java", "kafka")); CANDIDATES.put("Dara", Set.of("python", "kafka", "gcp"));
        CANDIDATES.put("Eli", Set.of("go")); CANDIDATES.put("Fatima", Set.of("python", "kafka", "aws", "terraform"));
    }
    static final List<String> LOG = new ArrayList<>();

    // The human checkpoint. Here the recruiter's answers are scripted; in production the run waits for them.
    static Object recruiter(String question, Object proposal) {
        Object answer = switch (question) {
            case "approve qualifications" -> proposal;
            case "advance which candidates" -> ((List<?>) proposal).stream().map(Object::toString).filter(c -> !c.equals("Dara")).collect(Collectors.toList()); // recruiter knows Dara just accepted another offer
            default -> null; // "apply learned change": not reviewed yet
        };
        String shown = answer == null ? "pending" : answer instanceof List<?> l ? l.stream().map(Object::toString).collect(Collectors.joining(", ")) : answer.toString();
        LOG.add("  [human] " + question + ": " + shown);
        return answer;
    }

    static List<String> sourcing(List<String> must) {
        return CANDIDATES.entrySet().stream().filter(e -> e.getValue().stream().anyMatch(must::contains)).map(Map.Entry::getKey).collect(Collectors.toList());
    }

    static List<String> evaluation(List<String> found, List<String> must) {
        List<String> shortlist = new ArrayList<>();
        for (String c : found) {
            List<String> evidence = CANDIDATES.get(c).stream().filter(must::contains).sorted().collect(Collectors.toList());
            LOG.add("  evaluation: " + c + " matches " + evidence.size() + "/" + must.size() + " (" + String.join(", ", evidence) + ")");
            if (evidence.size() == must.size()) shortlist.add(c);
        }
        return shortlist;
    }

    // The supervisor plans every step up front, then executes them in order (plan-and-execute).
    static List<String> plan(String request) { return List.of("intake", "sourcing", "evaluation", "review", "outreach", "learning"); }

    @SuppressWarnings("unchecked")
    public static void main(String[] args) {
        List<String> must = List.of(), found = List.of(), shortlist = List.of(), advance = List.of();
        for (String step : plan("Senior backend engineer, streaming")) {
            LOG.add("step: " + step);
            switch (step) {
                case "intake" -> must = (List<String>) recruiter("approve qualifications", List.of("kafka", "python"));
                case "sourcing" -> { found = sourcing(must); LOG.add("  sourcing: " + found.size() + " candidates found"); }
                case "evaluation" -> shortlist = evaluation(found, must);
                case "review" -> advance = (List<String>) recruiter("advance which candidates", shortlist);
                case "outreach" -> LOG.add("  outreach: messages drafted for " + String.join(", ", advance));
                default -> {
                    String proposal = "add 'aws' as a preferred skill";
                    LOG.add("  learning: recruiter advanced only AWS users -> proposes: " + proposal);
                    if (recruiter("apply learned change", proposal) == null) LOG.add("  learning: change held until the recruiter approves it");
                }
            }
        }
        System.out.println(String.join("\n", LOG));
    }
}
step: intake
  [human] approve qualifications: kafka, python
step: sourcing
  sourcing: 5 candidates found
step: evaluation
  evaluation: Asha matches 2/2 (kafka, python)
  evaluation: Ben matches 1/2 (python)
  evaluation: Chen matches 1/2 (kafka)
  evaluation: Dara matches 2/2 (kafka, python)
  evaluation: Fatima matches 2/2 (kafka, python)
step: review
  [human] advance which candidates: Asha, Fatima
step: outreach
  outreach: messages drafted for Asha, Fatima
step: learning
  learning: recruiter advanced only AWS users -> proposes: add 'aws' as a preferred skill
  [human] apply learned change: pending
  learning: change held until the recruiter approves it

5. Try it: which steps need a human?

For each step in a hiring agent, decide: let the agent do it, or require the recruiter's approval.

    6. Why these choices fit

    7. Lessons

    8. Your turn

    1. Map a workflow (10 min). Pick a multi-step process at work. List its stages as a plan an agent could write up front.
    2. Mark the accountable decisions (10 min). Which steps would a person have to answer for? Those get approval gates.
    3. Design the evidence (10 min). For each gate, what must the agent show so the approver can decide quickly and correctly?
    Say it in the interview
    • "How would you design an agent for a regulated or people-facing workflow?" Plan-and-execute under a supervisor, so the steps are explicit and inspectable, with specialised sub-agents or tools for each stage, and human approval on every decision a person is accountable for. LinkedIn's Hiring Assistant is built that way, including approval before learned changes apply.

    9. Check yourself

    Why did LinkedIn choose plan-and-execute?

    An explicit plan makes the agent think through all steps; an executor then carries them out.

    What does the supervisor do in Hiring Assistant?

    All interactions flow through it: the post calls it the central nervous system.

    When are changes proposed by the learning agent applied?

    An agent learning from human choices can infer the wrong rule.

    Which decisions keep a human in the loop, by LinkedIn's principle?

    Accountability, not the step's type, decides where the gate goes.

    10. Sources and further reading

    ResourceWhy
    How we engineered LinkedIn's Hiring Assistant (LinkedIn Engineering, October 2025)Every fact and quote on this page.
    Module 07a: Agent architecturesPlan-and-execute, supervisors and human gates in general.
    C11 and C12When to split work across agents, and when not to.