> ## Documentation Index
> Fetch the complete documentation index at: https://f4c7a9e2d8b1-docs.tenzo.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Candidate history

> What Tenzo knows about one application, and the named facts read from it

<Warning>
  **Restricted Access**: This documentation is only accessible to @tenzo.ai and @salv.ai email addresses.
</Warning>

Candidate history is what Tenzo knows about one application (a candidate on a job): where it
has been, how it entered, and what each module found. Stage-action writebacks (TEN-1540) and
conditions (TEN-1591) will read it through the [fact catalog](#fact-catalog). Nothing calls
it yet.

It is a read model over rows Tenzo already stores. The one value stored for it is the resume
auto-pass reason, which had no clean source. `load_candidate_histories` in
`candidate_history/candidate_history_service.py` reads through DAOs in two concurrent rounds,
with a fixed number of queries however many application ids it gets, and a pure builder turns
those rows into one `CandidateHistory` per application. Unknown or deleted applications, and
applications whose candidate row is missing, are left out.

An empty value is a real value: "no interview yet" is not 0% completion.

## Path

Where the application is and where it came from, from `tenzo_stage_history` in `history_seq`
order.

| Field | Meaning |
| - | - |
| `current_stage` | The application's stage now |
| `entered_current_stage_at` | When it landed there. Empty when the latest history row is not that stage |
| `previous_stage` | The stage it came from on that landing |
| `visits` | Every landing, oldest first, with the stage it came from, the trigger and the call |

## Modules

One entry per module run on the application (`candidate_module_runs`): the module, its work
state, its handoff outcome, any override, and when it started and finished. Applications
enrolled before module runs existed have none.

## Ingest

How the application entered Tenzo and whether the candidate can be reached.

| Field | Meaning |
| - | - |
| `candidate_source` | The application's `candidate_source` |
| `sourced` | Sourced, uploaded or tearsheet rather than an applicant. Uses `is_sourced_candidate()`, the rule Resume Review uses to auto-pass sourced candidates |
| `has_ats_application` | The application has an ATS application id |
| `origin_stage` | The first stage the application is known to have been on |
| `can_send_sms`, `can_send_email` | The outreach gates' current verdicts (`can_send_sms`, `can_send_email`), including an application-level SMS answer where one exists |

Most applications are inserted on their first stage without a history row, so
`origin_stage` is the first recorded move's "from" stage. When that row has none it is the
first landing, and with no history it is the current stage. Consent is the current value, not
the value at entry.

## Job

`paused` is true when the job's status is Paused. It is empty when the job is deleted, and it
is the current status, not the status at any past event.

## Resume Review

| Field | Meaning |
| - | - |
| `result` | `passed`, `auto_passed` or `failed`. Empty when nothing records a review |
| `result_source` | Which record the result came from (below) |
| `auto_pass_reason` | Why the review auto-passed (below). Only for an auto-pass |
| `star_rating` | 0 to 5. 0 is a disqualifying mismatch |
| `meets_all_required` | No when a required requirement is not a match |
| `failed_required_requirements`, `failed_star_threshold`, `failed_radius` | The failed checks scoring recorded |

The rating and checks come from `candidate_resume_scores`. The result is read from the first
record that has one:

1. A finished Resume Review module run: passed, rejected, or bypassed (auto-passed).
   Overridden and re-review runs no longer say what scoring found, so they are skipped.
2. The score row: failed when any failed check is set, passed otherwise. Most applications
   take this path, because module runs exist only on the module-owned scoring path.
3. The path: a landing on Resume Rejected.

These describe what the review found. A recruiter override, such as Progress to Interview,
does not change them.

### Auto-pass reason

`candidate_campaign_info.resume_auto_pass_reason` holds an `AutoPassReason`: `sourced`,
`no_resume`, `no_requirements`, `no_job_description`, `disqualification_disabled`,
`skip_disqualification` or `review_turned_off`. Resume Review writes it in the same
transaction as its continuing publish: an auto-pass writes its reason and every other
continuing result clears it. A pass that skips scoring records the rule that applied, checked
in this order: skip disqualification, sourced, review turned off on the job, disqualification
disabled.

Candidate history reports the reason only when the result is `auto_passed`, so a value left
by a rejection is never read. Auto-passes from before the column existed, and the legacy
scoring path, have none.

## AI Interview

| Field | Meaning |
| - | - |
| `status` | `not_started`, `started`, `completed`, `failed_knockout` or `skipped` |
| `call_id` | The call the numbers come from |
| `overall_grade` | The interview score |
| `completion_rate` | 0 to 100, rounded half up to a whole number |
| `proctoring_score` | Web interviews only |
| `web_interview` | Web or phone |

The numbers come from the call on the latest Call Completed or Failed Knockout landing in the
path. That is the call today's post-call ATS writeback uses, and it carries the application's
final numbers: the post-processor copies a merged assessment onto it, and regrades and metric
edits rewrite it. Test calls are ignored. A regrade that flips the knockout lands the other
stage, so the latest landing decides the status.

Without such a landing the numbers are empty and the status follows the AI Interview module
run (bypassed is `skipped`; outreach or scheduled is `started`), or, with no run, the path (a
visit to Outreach or AI Interview Scheduled is `started`). An incomplete call that never lands
Call Completed therefore leaves the numbers empty.

## Scheduling

The follow-up meeting Scheduling asks the candidate to book after the interview.

| Field | Meaning |
| - | - |
| `status` | `not_started`, `requested`, `booked` or `stopped` |
| `meeting_start`, `interviewer_email` | The current booking. Only when `status` is `booked` |
| `reminder_count` | Scheduling reminders sent |
| `stop_reason` | `no_response`, `no_recruiter_availability` or `other`. Only when `status` is `stopped` |

Status comes from the Scheduling module run. Follow-ups from before module runs existed have
only the application row, so status falls back to the application's follow-up status, or
`booked` when a booking exists. Booking details come from `scheduled_follow_up_interviews`,
which holds only the current booking, so a cancelled meeting clears them. The stop reason is
mapped from `scheduling_reminder_status`: Scheduling stops only when reminders run out or no
recruiter has availability.

## Fact catalog

`candidate_history/fact_catalog.py` names the facts conditions and writebacks use, so every
consumer reads the same fact the same way. Adding a fact is one `FactKey` and one
`FactDefinition`.

Each definition has:

* a stable key, namespaced by its group (`ai_interview.completion_rate`). Keys are persisted
  in conditions and templates, so a key's value is never renamed
* a group: `path`, `ingest`, `job`, `resume_review`, `ai_interview` or `scheduling`. Module
  groups name the module that records them
* a type: `number`, `yes_no`, `choice`, `date` (timezone-aware) or `text`. Text facts are
  variables only and are not compared in conditions
* an English label, which the UI translates by key
* the allowed values for a choice fact, taken from the enum it reads, and an optional range
  for a number fact (completion 0 to 100, star rating 0 to 5)
* a pure reader over `CandidateHistory`

| Group | Facts |
| - | - |
| `path` | `current_stage`, `previous_stage` |
| `ingest` | `sourced`, `has_ats_application`, `origin_stage`, `can_send_sms`, `can_send_email` |
| `job` | `paused` |
| `resume_review` | `result`, `auto_pass_reason`, `star_rating`, `meets_all_required`, `failed_required_requirements`, `failed_star_threshold`, `failed_radius` |
| `ai_interview` | `status`, `overall_grade`, `completion_rate`, `proctoring_score`, `web_interview` |
| `scheduling` | `status`, `meeting_start`, `interviewer_email`, `reminder_count`, `stop_reason` |

`read_fact` returns a fact's value, empty when it is not recorded yet, and raises
`FactTypeMismatchError` when stored data breaks the fact's declared type rather than passing a
bad value on. `read_facts` reads them all, and `facts_in_group` lists a group's facts for an
editor picker.

## Not recorded yet

TEN-1320 also lists facts that have no clean source today and are left until a condition needs
them: entry from a configured ATS start stage, whether a call was a test call, the
work-accommodation flag, interview reuse, and whether scheduling is enabled on the job (stored
only in Cosmos).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.