Home

/

Build It in a Weekend. Run It for Years.

/

Reformatting into your branded template

Reformatting into your branded template

Chapter 6
Part II
3
min read

Reformatting into your branded template

Once a candidate's CV has been redacted into a safe, structured object, the model earns its keep on the cosmetic job: turning that object into a polished, on-brand document. We give it the allowlisted fields and your house template, and ask it to write clean, consistent prose: a tidy professional summary, experience bullets in your tone, dates normalised.

Every model call routes through the guarded ILlmGateway, the same gateway every workflow uses. It enforces the allowlist, runs a DLP inspection on what's about to go out, and fails closed if anything looks like PII slipped in. It never calls the model SDK raw. (Illustrative excerpt; the gateway internals live in the security and compliance chapter.)

public async Task<string> FormatForClientAsync(ClientCv cv, CancellationToken ct)
{
    // Gateway inspects every field against the allowlist, runs DLP,
    // and fails closed if anything off-list (e.g. a stray phone no.) appears.
    var request = new LlmRequest("FormatCv")
        .WithStructuredInput(cv)              // structured, not raw text
        .WithTemplate(_house.BrandedTemplate);

    var result = await _gateway.InvokeAsync(request, ct); // never the SDK directly
    return result.Content; // branded, redacted CV body — ready for human review
}

The Semantic Kernel function behind it is a plain prompt that takes named inputs and returns the formatted body. Because it only ever receives the safe fields, even a perfectly-worded prompt-injection buried in the candidate's CV has nothing sensitive to exfiltrate. There's nothing in context to leak.

Pulling it from the ATS — and writing it back

In practice the CV doesn't arrive as a stray file; it lives on a candidate record in your ATS. The flow is: fetch the file, extract to CandidateProfile, build the client copy, format, then attach the branded version back to the record for a human to release.

Bullhorn (YS flagship), fetch the candidate's CV file, push the formatted copy back:

// Bullhorn REST: read the file, write the branded version back as a new file.
var file = await _bullhorn.GetAsync(
    $"file/Candidate/{candidateId}/{fileId}/raw");          // raw doc stays server-side
var profile = await _extractor.ExtractAsync(file);          // -> CandidateProfile
var branded = await FormatForClientAsync(BuildClientCopy(profile), ct);

await _bullhorn.PutFileAsync(
    $"file/Candidate/{candidateId}", branded.AsPdf(),
    description: "Branded client CV (redacted) — pending review");

JobAdder uses the same shape against its REST v2 endpoints. Calls are prefixed with the per-account api base URL returned at token time (don't hardcode the host), with Authorization: Bearer {access_token} on every request:

// Fetch the latest resume binary, then record the draft back as a note.
var att = (await _jobAdder.GetAsync(
    $"candidates/{candidateId}/attachments?type=Resume&latest=true"))
    .Items.First();                                         // {attachmentId, ...}
var doc = await _jobAdder.GetBytesAsync(
    $"candidates/{candidateId}/attachments/{att.AttachmentId}"); // raw binary
var profile = await _extractor.ExtractAsync(doc);
var branded = await FormatForClientAsync(BuildClientCopy(profile), ct);

// JobAdder has no separate "activity" resource — activities are notes.
await _jobAdder.PostAsync($"candidates/{candidateId}/notes",
    new { text = "Branded client CV (redacted) — pending review" });

Both write the result back as a draft, pending review, not as something the client can already see. Which brings us to the only step that actually matters.

The leash sits on send

The agent does everything up to the client's inbox and then stops. It produces a branded, redacted CV, attaches it to the record, and flags it for release. A human opens it, glances at it, and clicks send.

That glance is the point. Not because the agent is unreliable (by the time you've shipped this, it's more consistent than a tired recruiter at 6pm) but because send to client is a consequential, irreversible action involving someone else's personal data. You don't automate that. You automate everything leading up to it.

Automate the formatting. Automate the redaction. Never automate the send.

So the four hours a week come back, the personal data stays in the building until someone decides otherwise, and the redaction stopped being a thing you remember to do and became a thing the system simply does. The recruiter's job shrank to the one second that carries the risk.

That's a cog. One workflow, doing one job, earning its keep, with the leash exactly where the consequence is. Build it in an afternoon. The hard part, as ever, is keeping that DLP inspection honest as your CVs, your clients, and the model underneath it all keep changing.

Next: from one CV to the whole stack. Ranking a batch against a single req and handing back a shortlist.

the-math-no-recruiter-can-win-by-hand
what-an-ai-agent-actually-is
the-leash
the-toolkit
the-model-small-capable-swappable
talking-to-your-ats
use-case-1-resume-screening-against-a-job
the-shape-of-the-loop
running-it-thought-action-observation
use-case-2-cv-formatting-redacting-for-clients
reformatting-into-your-branded-template
resume-shortlisting
that-was-easy
security-compliance
keeping-pii-out-of-the-llm
exceptions-reliability
silent-api-drift-the-ats-changes-under-you
when-it-fails-anyway-dead-letter-and-the-leash
monitoring-observability
maintenance-the-lifecycle
the-scorecard-success-metrics-kpis
build-vs-buy-vs-managed
what-an-engineer-actually-costs
what-the-wider-data-says-happens-next
conclusion-how-this-gets-run-for-you
the-promises-behind-the-service
fuller-code-listings
one-full-screening-react-loop-semantic-kernel
env-deployment-reference
secrets-in-dev-vs-production
bullhorn-jobadder-endpoint-cheat-sheets
sources-further-reading
compliance-primary-law-sources

Download the full PDF for free?

Download full PDF
build-it-in-a-weekend.pdf
Oops! Something went wrong while submitting the form.
Related Chapters