Section B · Failure modes

Error Handling for HR Agents

What goes wrong, how it goes wrong, and the fail-safe defaults that keep an agent from quietly causing payroll, legal, or trust damage.

Why HR is unforgiving

An error in a search-ranking model returns a worse result and the user reformulates. An error in an HR agent:

  • Changes the wrong person's record
  • Triggers an off-cycle payroll adjustment
  • Sends a benefits-eligibility email to a recently terminated employee
  • Surfaces another employee's salary in a Slack thread
  • Pushes an EU employee's PII to a US endpoint without a DPA

None of these recover by themselves. Every one creates work for HRKX, Payroll, IT, or Legal. The right mental model is closer to "this is a system that touches money and identity" than to "this is a chatbot."

Five classes of failure to design against

  1. Wrong worker affected — agent took the right action on the wrong identity
  2. Payroll-calendar misalignment — right action, wrong timing window
  3. Broken Workday integration — transport / auth / schema failure mid-run
  4. EIB partial failure — bulk operation succeeded for some rows, failed for others
  5. PII leak — data crossed a boundary it shouldn't have (logs, summaries, third-party APIs)

Each has a specific design response, below. In interview, when asked "how do you handle errors," step through this list.

Wrong worker affected — the cardinal HR failure

How it happens:

  • Two workers with the same name; agent picked the wrong one
  • Rehire detection failed; agent created a duplicate
  • Email-to-ID lookup hit a stale record
  • Slack-user-id to worker-id mapping was wrong because someone changed their Slack handle

Design responses

  • Always anchor on Workday's stable identifier — Worker ID, not name, not email-as-string, not Slack handle. All other identifiers are lookups to the Worker ID, never replacements.
  • Surface identity in the HITL preview — "You are about to change FTE for Jane Doe, W-12345, hired 2024-03-01, currently in Wallet Eng / EMEA." The approver catches the wrong-Jane case in 5 seconds.
  • Rehire detection on every hire-shaped action — match by email hash + DOB hash + government ID hash; if any high-confidence match, halt and route.
  • Confirmation echo — Slack agent always echoes "Got it — looking up @yourname (Jane Doe, W-12345). Confirm?" for any action-shaped query.
The HITL preview is your last line of defense

Design the approval UI so the right answer is fast and the wrong answer is impossible to miss. A diff view, identity context, effective date, jurisdiction, and the reason code — all visible without scrolling.

Payroll-calendar misalignment

The agent does the right thing at the wrong time. Examples:

  • Comp change committed at T-2 days before US payroll cutoff doesn't make this cycle, lands in next — wage is wrong for one pay period.
  • Termination effective today, but payroll already locked — the worker is paid for a day they shouldn't be, or worse, their final-pay calc is off.
  • Retro change committed creates a retro-pay event that surprises Payroll.

Design responses

  • Payroll calendar is a tool input, not an afterthought — every proposed write tool requires effective_date and checks it against get_payroll_lock_windows(country).
  • Lock-window behavior is explicit refusal + escalation — not silent queue, not "try anyway."
  • Multi-country awareness — UK lock and US lock are different. The agent reasons per the worker's payroll country, not a global default.
  • Retro changes always tier up — past effective dates require Payroll Ops co-approval, regardless of magnitude.
def check_payroll_window(effective_date: date, country: str) -> dict:
    """Return safe / caution / locked for this date + country combo."""
    lock = payroll_calendar.get_lock_window(country, effective_date)
    days_to_cutoff = (lock["cutoff"] - date.today()).days
    if effective_date < date.today():
        return {"status": "retro", "tier_up": True, "rationale": "retro change requires Payroll co-approval"}
    if days_to_cutoff < 0:
        return {"status": "locked", "halt": True, "rationale": f"payroll locked since {lock['cutoff']}"}
    if days_to_cutoff <= 3:
        return {"status": "caution", "tier_up": True, "rationale": f"{days_to_cutoff} days to cutoff"}
    return {"status": "safe"}

Broken Workday integration

How it happens:

  • Workday API returns 5xx mid-run
  • Auth token expired (Workday tokens are aggressive about session timeouts)
  • Schema mismatch — the field your code expects was renamed in a tenant change
  • RaaS report returned different columns than yesterday because someone edited the report

Design responses

  • Pin to specific RaaS report IDs and versions, not free-text URLs
  • Validate response schema on every read — fail fast if the schema diverged
  • Retry transient 5xx with backoff, but cap attempts; surface persistent failures as a halt
  • Token refresh is a first-class concern — not "retry once and hope"
  • Circuit-breaker on Workday API — if 5 consecutive failures, halt all Workday-touching agents until ops acknowledges
Why the circuit breaker matters

During a Workday tenant maintenance window or a transient outage, you do not want 14 agents quietly piling up queued retries that all fire at once when the tenant returns. A circuit breaker keeps the blast radius small.

EIB partial failure

You upload 50 rows. 47 succeed. 3 fail with per-row errors. This is the normal case, not the exception.

Design responses

  • Track proposed-action IDs end-to-end — each EIB row carries the proposal_id; the EIB run summary correlates back
  • Surface failures per row with the Workday error reason and the original proposal
  • Re-run only failures, not the whole batch — using the same idempotency keys
  • HRKX-readable failure dashboard — not "see the EIB run summary in Workday"
  • Never auto-retry payroll-affecting failures — failures route to a human, who decides whether to reattempt or cancel
def process_eib_results(eib_run_id: str, proposals: list[dict]):
    """Reconcile an EIB run with the original proposals."""
    summary = workday.get_eib_run_summary(eib_run_id)
    for row in summary["rows"]:
        proposal = find_proposal_by_id(proposals, row["external_ref"])
        if row["status"] == "SUCCESS":
            mark_proposal_committed(proposal["id"], result=row)
        else:
            mark_proposal_failed(proposal["id"], reason=row["error"])
            # NOT auto-retry. Route to ops.
            notify_ops_channel(
                kind="eib_row_failure",
                proposal=proposal,
                reason=row["error"],
                workday_run_id=eib_run_id,
            )

PII leak

The quietest, most consequential failure class. Vectors:

  • Logs contain raw PII (SSN, salary, home address)
  • An LLM call sent EU employee data to a US-hosted endpoint without DPA coverage
  • A Slack post named another employee in front of an audience that shouldn't see it
  • An error message included raw worker data in a downstream alert
  • An MCP server's "list" tool returned more than the agent's caller was authorized for

Design responses

  • Log redaction at write time, not read time — never write raw SSN, raw salary above a threshold, or any document marked Restricted to logs
  • Authorization at MCP boundary — the MCP server enforces who can read what, not the agent
  • Data residency aware tooling — EU PII goes to EU model endpoints; the integration layer enforces this, not the prompt
  • Error messages strip sensitive context — alerts say "proposal P-123 failed for worker_id_hash:abc123," not "Jane Doe's SSN didn't validate"
  • Output redaction step before any human-visible surface — even drafts route through a PII-redaction pass for cross-employee references

Fail-safe defaults — the operating principles

Make these defaults explicit in your interview. They are the difference between a senior architect and a "shipping" mindset that's wrong for HR.

DefaultRationale
On any uncertainty, halt and escalate. Never auto-commit.HR errors don't bounce back; they accrue silently.
Default model temperature for payroll-adjacent tasks: 0.Determinism aids audit and reproducibility.
Default retry policy for committing tools: zero auto-retries; human decides.Partial-effect retries cause duplicate state.
Default behavior near payroll cutoff: refuse, route, never queue silently."It'll process next cycle" is rarely the right answer.
Default for non-deterministic tool failures: circuit-break, page on-call.Cascading retries are worse than backpressure.
Default for an output that fails schema validation: do not present to a human approver.The HITL UI's promise is "this is a valid proposal." Don't break it.

Never auto-trigger near a payroll cutoff

This deserves its own section. In interviews, when given an aggressive design prompt ("automate this end-to-end"), reach for this principle unprompted:

The rule

Any agent action that has any chance of touching payroll data, comp, FTE, leaves, or worker type — and is within the lock window of the worker's payroll country — defaults to halt + escalate. Not "process at lower priority." Not "queue for after cutoff." Halt. Escalate. Wait for human direction.

The exception list is short and explicit, and is owned by the Process Owner, not the agent. Examples:

  • HRBP and Payroll co-approval explicitly waives the lock window for this single proposal
  • The action is reversible and Payroll has pre-authorized its automation (rare)

Incident playbook — when something does go wrong

You will be asked some version of "tell me about an incident." Have a real story. The structure that works:

  1. Detect — what alert fired, who saw it, how fast
  2. Contain — what you turned off, what you paused, what you rolled back
  3. Communicate — who you told, in what order (HRBP, Payroll, Legal if PII)
  4. Diagnose — root cause analysis
  5. Fix — patch and verify
  6. Prevent — code change, eval addition, runbook update
  7. Postmortem — blameless writeup, action items tracked

For an HR-specific incident, the communication step is loud — Payroll, HRBP, and possibly Legal must hear from you before they hear from an affected employee. The first 30 minutes are about not making it worse: halt all running agents, freeze any in-flight writes, preserve audit logs untouched.

Interview framings worth memorizing

  • "The cardinal HR failure is wrong-worker. I anchor on Workday Worker ID, surface full identity context in HITL previews, and run rehire-detection on every hire-shaped action."
  • "My agent default near payroll cutoff is halt + escalate. Not retry. Not queue. The cost of a late change is paid in payroll exceptions; the cost of one missed automation is one missed automation."
  • "EIB partial failure is the normal case. Per-row reconciliation, per-row idempotency keys, no auto-retry on payroll-affecting failures."
  • "PII leakage is zero-tolerance in tests and zero-retry in production. Redaction happens at log write time, authorization at MCP boundary, residency at the integration layer."
  • "On any uncertainty, the agent's right move is halt. Errors here don't bounce back — they accrue."