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.
- Sam What does a production agent look like in a big company?
- Priya LinkedIn's Hiring Assistant is a good example. It helps recruiters turn a role into qualifications, find candidates, assess them and draft outreach. In October twenty twenty-five its engineers described how they built it, their first production agent.
- Sam What did they choose?
- Priya Plan-and-execute, because, in their words, ReAct alone is not sufficient for an enterprise-grade agent. A planner writes a structured plan and an executor carries out each step. A supervisor, which they call the central nervous system, interprets the recruiter's input and delegates to specialised sub-agents: intake, sourcing, evaluation, outreach, screening, learning and memory. Some are just tools. And the principle: humans stay in the loop for all decisions they're accountable for.
- Sam How does one request flow?
- Priya The supervisor writes the plan. Intake drafts the qualifications, and the recruiter approves them. Sourcing finds candidates, and evaluation assesses each one against the approved qualifications, showing its evidence. The recruiter decides who advances. Outreach drafts messages. And when the learning agent proposes a change from what it observed, the change waits for the recruiter's approval.
- Sam Why does the learning step need approval?
- Priya The demo shows it. The recruiter drops one shortlisted candidate for a reason the agent can't know, and the learning step draws the wrong lesson, that only AWS users get advanced. Because changes wait for approval, the wrong rule never applies.
- Priya Your turn. Which needs the recruiter: searching the candidate pool, or changing the criteria?
- Priya Changing the criteria. Searching is read-only within approved rules.
- Sam What should I take away?
- Priya Pick plan-and-execute when the steps can be listed and thoroughness matters. Gate decisions by accountability, not by step type. Make the agent show its evidence so approvals are quick and correct. Treat learning as a proposal. And compare it with Anthropic's system: the same supervisor architecture, but a planned sequence with approvals instead of a parallel fan-out.
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.
2. What they chose, and why
| Choice | From the post |
|---|---|
| Plan-and-execute | They 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-agents | Intake, sourcing, evaluation, candidate outreach, candidate screening, learning and cognitive memory. Some are modelled as tools rather than separate agents. |
| Humans in the loop | The 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
- Plan-and-execute fits a known workflow. Hiring has recognisable stages, so writing the plan first makes the agent cover every step and makes its behaviour easier to inspect than a free-running loop.
- A supervisor fits one front door. The recruiter talks to one assistant; specialists stay behind it. That's the opposite of handoffs, where the user would talk to each specialist.
- Sub-agents as tools keep it simple. Not everything needs its own agent identity; a specialist that does one bounded job can be a tool the supervisor calls.
- Human approval follows accountability. The agent proposes and explains; people decide anything they'd answer for. Learned changes are proposals too.
7. Lessons
- Pick plan-and-execute when the steps can be listed and thoroughness matters more than improvisation (Module 07a).
- Gate decisions by accountability, not by step. Searching and drafting can run; advancing a candidate or changing criteria needs a person.
- Make the agent show its evidence. A recruiter can only approve what they can check.
- Treat learning as a proposal. An agent learning from human choices can infer the wrong rule; keep a person in that loop.
- Compare: Anthropic's supervisor fans out in parallel for breadth (C11); LinkedIn's runs a planned sequence with approvals. Same architecture, different shapes of work.
8. Your turn
- Map a workflow (10 min). Pick a multi-step process at work. List its stages as a plan an agent could write up front.
- Mark the accountable decisions (10 min). Which steps would a person have to answer for? Those get approval gates.
- Design the evidence (10 min). For each gate, what must the agent show so the approver can decide quickly and correctly?
- "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?
What does the supervisor do in Hiring Assistant?
When are changes proposed by the learning agent applied?
Which decisions keep a human in the loop, by LinkedIn's principle?
10. Sources and further reading
| Resource | Why |
|---|---|
| How we engineered LinkedIn's Hiring Assistant (LinkedIn Engineering, October 2025) | Every fact and quote on this page. |
| Module 07a: Agent architectures | Plan-and-execute, supervisors and human gates in general. |
| C11 and C12 | When to split work across agents, and when not to. |