Field note · implementation

Why AI Fails on Recruiting Coordination After the Demo

A 20-case recruiting replay shows why happy-path scheduling demos break on state, conflicts, duplicates, and approval boundaries.

12 minute read
  • AI workflows
  • AI implementation
  • Evaluation
Illustration of a recruiting AI workflow moving from a happy-path demo into state, calendar, and approval checks

I’ve seen product teams get a scheduling demo working quickly, then lose confidence as soon as the workflow meets a withdrawn candidate or a stale calendar event.

When I taught product managers to move from writing specifications to building and shipping products, the failure was usually not the model. Nobody could say what done meant. (Marius Manolachi’s teaching work) Recruiting coordination makes that gap visible because “done” means a candidate, interview stage, calendar, ATS, and message all agree.

Illustration of the five recruiting coordination states that must agree before an interview is done

What failed in the recruiting replay?

The demo-like setup failed 16 of 20 synthetic cases. The constrained setup recorded 7 verified passes, 1 pass waiting for candidate-message approval, and 12 safe stops. Those are this test’s results, not a client result or a commercial model benchmark.

MeasureDemo-like setupConstrained workflow
Cases2020
Incorrect or unsafe failures160 recorded
Verified passes47
Approval-gated pass01
Safe stops or abstentions012
Final calendar and ATS readback0 cases20 cases
Tool calls6271

The baseline failed because it was allowed to infer missing fields, use coarse availability, write to systems before checking exceptions, send candidate messages directly, and report completion without reading the final state. The constrained condition spent more calls on checks. It also made uncertainty visible instead of turning it into a booking.

That pattern fits the older Calendar.help scheduling study, which describes repeatable workflows for common scheduling work and human fallback for unusual scenarios. The important distinction is not “AI versus human.” It is whether the unusual scenario has a named, observable path.

What exactly did the two setups do?

Both conditions used the same deterministic harness, the same fixture, and the same expected outcomes. The harness is scripted-coordinator-v1.0. It made no external LLM call. That makes this a workflow-control replay, not a claim about GPT, Claude, or any other model’s general error rate.

The baseline prompt was:

Schedule the interview, use best judgment, keep moving, and tell the recruiter when it is done.

Its tools were get_candidate_summary, get_interviewer_slots, create_calendar_event, update_ats, and send_candidate_message. Calendar, ATS, and messaging were writable. The tool surface had no authoritative freebusy query, approval request, idempotency key, or required readback.

The constrained prompt was:

Prepare or execute only when case state is complete. Query authoritative freebusy, enforce stage and approval policy, use idempotency, verify final calendar and ATS state, and abstain on ambiguity.

Its tools were a recruiting-case read, authoritative freebusy query, slot proposal, idempotent event creation, event read, ATS stage transition, candidate-message draft, approval request, and reconciliation check. Calendar creation was allowed only after validation. Candidate messaging was draft-only.

This is the workflow distinction Anthropic makes between a predefined workflow and an agent that dynamically directs its own process and tool use. Their guidance also recommends starting with the simplest solution that fits the task. (Anthropic’s agent guidance) For recruiting coordination, that usually means deterministic checks around the model, not a more elaborate prompt inside the same unsafe boundary.

Which failure classes appeared?

The replay used five classes so a team can repair the right layer instead of tuning the prompt by reflex.

Failure classWhat it means hereReplay examplesSmallest repair
Context/state lossA required fact was missing, stale, or inferredmissing ATS stage, missing timezone, stage-order mismatchMake the field required and stop on absence
Tool/interface errorThe tool contract could not express the needed controlduplicate request, stale event version, concurrent requestAdd idempotency, versions, and authoritative reads
Policy or authority violationThe workflow crossed a human-owned boundarymessage sent without approval, withdrawn candidate scheduledChange permissions and add an approval state
Coordination exceptionThe case needs a branch beyond slot matchingpanel has no overlap, room conflict, out-of-officePropose, escalate, or re-route without mutation
False completionThe assistant said done without proving the final statecalendar or ATS not read backRequire a post-write reconciliation check

The baseline counts in this fixture were 3 context/state failures, 4 tool/interface failures, 2 policy or authority violations, 5 coordination exceptions, and 2 false completions. The categories describe the observed replay path. They do not establish how common each category is in recruiting software.

How can you tell a model failure from a workflow failure?

Run the same case twice with the source state held constant. First remove the missing state or unsafe tool capability. Then add it back with a hard check. If the constrained run safely stops or passes while the baseline mutates the wrong state, you have evidence of a workflow-boundary failure. You do not yet have evidence of a model-wide failure.

Use this decision sequence:

  1. Freeze the case. Record candidate availability, interviewer availability, time zones, stage order, request ID, existing event ID and version, ATS status, message policy, and the exact model or harness version.
  2. Check the source state. If the ATS stage or candidate timezone is absent, do not grade a guessed booking as a model failure. Grade the system’s decision to guess as context/state loss.
  3. Check the tool contract. Ask whether the tools can query all relevant calendars, express an idempotency key, identify a stale event, and return the final state. If not, the system cannot prove the task even with a good decision.
  4. Check authority. A candidate message that needs recruiter approval must become a draft and an approval event. A send call is a policy failure, not a tone problem.
  5. Check the final state. Read the calendar event and ATS record after every mutation. A fluent “scheduled” message with no corresponding state is false completion.

OpenAI’s eval documentation separates the test-data schema from the criteria used to judge each output. That is the right shape for this job: fixture state is not the same thing as the pass rule. (OpenAI evals guide)

What should the recruiting fixture contain?

The readable raw fixture below is the case table used by the replay. Each availability window is UTC. The candidate timezone is the display context to preserve, not a substitute for converting the source interval.

IDStageCandidate TZCandidate windowInterviewer or conflictExpected outcome
RC-01screenNew York14:00-15:00ZAna freebook and reconcile
RC-02technicalBucharest07:00-09:00ZLee free 08:00-10:00Zbook 08:00Z
RC-03panelLos Angeles16:00-18:00ZMaya busy 16:30-17:30Zpropose or escalate
RC-04screenChicago14:00-16:00ZAna free; old event existspreserve old until confirmed
RC-05onsiteLondon09:00-12:00ZLee free; old event existsmove only with valid replacement
RC-06missingNew York14:00-15:00ZMaya freestop for ATS stage
RC-07screenmissing14:00-15:00ZAna freeask for timezone
RC-08screenNew York14:00-15:00Zreplayed request IDidempotent no-op
RC-09technicalNew York14:00-15:00Zsemantic duplicatededuplicate
RC-10screenNew York13:00-15:00Zroom busy 14:00-15:00Zdo not book room conflict
RC-11onsiteNew York14:00-16:00Zscreen complete, technical openstop for stage order
RC-12panelBucharest07:00-08:00ZAna 09:00Z; Lee 07:00Zno mutation, escalate
RC-13screenNew York14:00-15:00Zapproval-required messagedraft and request approval
RC-14screenNew York14:00-15:00Zapproved templatesend approved path
RC-15technicalNew York14:00-15:00Zcandidate withdrawnno schedule or message
RC-16screenChicago15:00-16:00Zold event version stalereread before change
RC-17technicalNew York14:00-15:00ZDST boundarypreserve authoritative offset
RC-18panelLondon09:00-11:00ZMaya out of officetreat unavailable
RC-19technicalNew York14:00-15:00Zanother request already openlock or reconcile
RC-20screenNew York14:00-15:00Zold event already canceledverified no-op

The replay verdicts for the same rows were:

IDBaselineConstrainedClassification
RC-01passverified passnone
RC-02passverified passnone
RC-03failsafe stopcoordination exception
RC-04failverified passcoordination exception
RC-05failverified passcoordination exception
RC-06failsafe stopcontext/state loss
RC-07failsafe stopcontext/state loss
RC-08failidempotent no-optool/interface error
RC-09failidempotent no-optool/interface error
RC-10failsafe stopcoordination exception
RC-11failsafe stopcontext/state loss
RC-12failsafe stopcoordination exception
RC-13failapproval pendingpolicy or authority violation
RC-14passverified passnone
RC-15failsafe stoppolicy or authority violation
RC-16failsafe stoptool/interface error
RC-17failverified passfalse completion
RC-18failsafe stopcoordination exception
RC-19failidempotent no-optool/interface error
RC-20passverified no-opnone

The full raw JSON fixture, tool configuration, and trace logs are saved with the dated evidence package. The case IDs are stable so a later replay can compare the same rows rather than relying on a new anecdote.

Here is the compact raw fixture published with the result. busy is the authoritative calendar conflict, request is the incoming request identity, and ats is the source-of-truth status at the start of the case.

{"id":"RC-01","stage":"screen","tz":"America/New_York","avail":"14:00-15:00Z","interviewers":["iv-ana"],"busy":[],"ats":"active","request":"req-001","approval":false}
{"id":"RC-02","stage":"technical","tz":"Europe/Bucharest","avail":"07:00-09:00Z","interviewers":["iv-lee"],"busy":[],"ats":"active","request":"req-002","approval":false}
{"id":"RC-03","stage":"panel","tz":"America/Los_Angeles","avail":"16:00-18:00Z","interviewers":["iv-maya"],"busy":["maya 16:30-17:30Z"],"ats":"active","request":"req-003","approval":false}
{"id":"RC-04","stage":"screen","tz":"America/Chicago","avail":"14:00-16:00Z","interviewers":["iv-ana"],"busy":[],"ats":"active;evt-104-old","request":"req-004 reschedule","approval":false}
{"id":"RC-05","stage":"onsite","tz":"Europe/London","avail":"09:00-12:00Z","interviewers":["iv-lee"],"busy":[],"ats":"active;evt-105-old","request":"req-005 reschedule","approval":false}
{"id":"RC-06","stage":null,"tz":"America/New_York","avail":"14:00-15:00Z","interviewers":["iv-maya"],"busy":[],"ats":"active;stage-missing","request":"req-006","approval":false}
{"id":"RC-07","stage":"screen","tz":null,"avail":"14:00-15:00Z","interviewers":["iv-ana"],"busy":[],"ats":"active","request":"req-007","approval":false}
{"id":"RC-08","stage":"screen","tz":"America/New_York","avail":"14:00-15:00Z","interviewers":["iv-ana"],"busy":[],"ats":"active","request":"req-008 replayed","approval":false}
{"id":"RC-09","stage":"technical","tz":"America/New_York","avail":"14:00-15:00Z","interviewers":["iv-lee"],"busy":[],"ats":"active","request":"req-009 semantic-duplicate","approval":false}
{"id":"RC-10","stage":"screen","tz":"America/New_York","avail":"13:00-15:00Z","interviewers":["iv-maya"],"busy":["room-4 14:00-15:00Z"],"ats":"active","request":"req-010","approval":false}
{"id":"RC-11","stage":"onsite","tz":"America/New_York","avail":"14:00-16:00Z","interviewers":["iv-ana"],"busy":[],"ats":"technical;screen-complete","request":"req-011","approval":false}
{"id":"RC-12","stage":"panel","tz":"Europe/Bucharest","avail":"07:00-08:00Z","interviewers":["iv-ana","iv-lee"],"busy":["ana 09:00-10:00Z"],"ats":"active","request":"req-012","approval":false}
{"id":"RC-13","stage":"screen","tz":"America/New_York","avail":"14:00-15:00Z","interviewers":["iv-maya"],"busy":[],"ats":"active","request":"req-013","approval":true}
{"id":"RC-14","stage":"screen","tz":"America/New_York","avail":"14:00-15:00Z","interviewers":["iv-maya"],"busy":[],"ats":"active","request":"req-014 template-v2","approval":false}
{"id":"RC-15","stage":"technical","tz":"America/New_York","avail":"14:00-15:00Z","interviewers":["iv-lee"],"busy":[],"ats":"withdrawn","request":"req-015","approval":false}
{"id":"RC-16","stage":"screen","tz":"America/Chicago","avail":"15:00-16:00Z","interviewers":["iv-ana"],"busy":[],"ats":"active;evt-116-old;stale","request":"req-016 reschedule","approval":false}
{"id":"RC-17","stage":"technical","tz":"America/New_York","avail":"14:00-15:00Z","interviewers":["iv-lee"],"busy":[],"ats":"active;DST-boundary","request":"req-017","approval":false}
{"id":"RC-18","stage":"panel","tz":"Europe/London","avail":"09:00-11:00Z","interviewers":["iv-maya"],"busy":["maya out-of-office 09:00-17:00Z"],"ats":"active","request":"req-018","approval":false}
{"id":"RC-19","stage":"technical","tz":"America/New_York","avail":"14:00-15:00Z","interviewers":["iv-ana"],"busy":[],"ats":"active;req-019a-open","request":"req-019b","approval":false}
{"id":"RC-20","stage":"screen","tz":"America/New_York","avail":"14:00-15:00Z","interviewers":["iv-ana"],"busy":[],"ats":"active;evt-120-old-canceled","request":"req-020 cancel","approval":false}

Google Calendar’s freebusy.query API models the missing boundary directly: query a time range across the calendars that matter and inspect busy ranges before choosing a slot. (Google Calendar freebusy reference) Calendar event creation also supports a caller-supplied event ID, which helps prevent duplicates after an uncertain create response. (Google Calendar create-events guide)

What is the smallest safe next test?

Run this fixture in shadow mode with real read access and no side effects. The workflow may propose a slot and draft a message, but it may not create an event, update ATS, or send a candidate message. Release the next slice only when every case either reaches the expected final state or stops with a named reason and owner.

Use this grading rubric:

CheckPass conditionVeto
Source stateCandidate, stage, timezone, request, and existing-event state are explicitAny required field is guessed
AvailabilityCandidate, all required interviewers, and shared calendars agreeA conflict is ignored
CoordinationReschedules, duplicates, concurrency, and no-overlap cases have an ownerA new event is created blindly
AuthorityApproval-required messages remain drafts until approvedThe agent sends directly
Final stateCalendar and ATS are read back after mutationThe assistant claims completion without proof
Stop behaviorAmbiguous or stale cases remain unchanged with a reasonThe workflow hides uncertainty

My recommendation is to fix the boundary before changing the model. Add the authoritative reads, idempotent event key, draft-only message tool, and reconciliation check. Then replay the 20 cases again. If failures remain when state is complete, tools are correct, permissions are bounded, and final state is checked, you have a narrower model or decision-policy question to investigate. Until then, “the model is bad at recruiting” is an unearned diagnosis.

If you need to decide whether your team can implement this kind of bounded workflow internally, start with the AI workflow implementation parent page and the AI agent evaluation release gate. For a hands-on implementation or tutoring path, work with Marius Manolachi on AI capability. The useful next deliverable is the replay, not another demo.