This page walks through a single run of the AI resume-screening agent: how the loop reads the job and the CV, scores the candidate with visible reasons, and writes the verdict back to your ATS. Once the agent is wired up with automatic function calling switched on, kicking off a screen is almost anticlimactic.
var settings = new OpenAIPromptExecutionSettings
{
ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions
};
var verdict = await gateway.InvokeAgentAsync( // routes via ILlmGateway
kernel, instructions,
input: $"Screen candidate {candidateId} for job {jobId}.",
settings);Behind that one call, the loop runs itself. The trace reads like a junior teammate thinking out loud:
Thought: I need the role's requirements first.
Action: GetJob("4821")
Observe: "Senior .NET Engineer — 5+ yrs C#, Azure, team lead exp."
Thought: Now the candidate's CV.
Action: GetCandidateCv("90217")
Observe: "...7 years C#/.NET... led a 4-person squad... no cloud..."
Thought: 3 of 4 met; Azure not evidenced. Strong but not perfect.
Action: SaveVerdict("90217","4821", { score:78, rec:"Shortlist",
reasons:[ "C#: Met — '7 years C#/.NET'",
"Team lead: Met — 'led a 4-person squad'",
"Azure: Not met — no cloud experience on CV" ] })That trace is the product. Not the 78. The four lines under it. A recruiter reads them in the time it takes to scroll, and either nods or overrules. The agent did the tireless reading; the human owns the moment of consequence.
This is also where the speed shows up. In one published YS demo, a screening agent of exactly this shape cleared 45 CVs in about 52 seconds: twenty shortlisted, fifteen rejected, ten flagged for a second human look. The morning's triage, done before the kettle boils, with every verdict carrying its reasons.
The result lands in the ATS as a note against the candidate, linked to the job, so it lives where your team already works rather than in some side dashboard nobody opens.
Bullhorn takes a Note via PUT entity/Note, with personReference pointing at the candidate and jobOrder at the role:
var body = new {
action = "AI Screening",
comments = result.ToHumanReadable(), // score + the met/missing reasons
personReference = new { id = candidateId },
jobOrder = new { id = jobId }
};
await _http.PutAsJsonAsync($"{restUrl}entity/Note?BhRestToken={token}", body);JobAdder has no separate "activity" resource (activities are notes), so the verdict goes back as a note with the same bearer token (POST {apiBase}/candidates/{id}/notes, body { text, type? }, with text required). Same verdict, same reasons, second rail.
Two things stay non-negotiable. First, the note is advice, not action: nothing about a "Reject" verdict changes the candidate's status, sends an email, or closes a door. A human does that, or it doesn't happen. Second, every model call (reading the CV, producing the verdict) went through the single ILlmGateway, which allowlists the structured fields that may leave the building, runs them past a DLP check, and fails closed if anything looks wrong. We never hand a raw CV to a model SDK and hope. Why that gateway matters, and what it costs to keep it honest, is the spine of the security and compliance work.
The agent reads, scores, and explains. It does not decide, and it never acts alone. That's the leash, and you can see it in every line above.
One cog, doing one job, with its reasoning in the open and a human on the only call that matters. Now we hand the chosen CV to the next cog, the one that has to leave the building.
Next: Use Case 2, turning that messy CV into your branded template, and stripping the personal details that must never reach a client.
Download the full PDF for free?