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, fromtenzo_stage_history in history_seq
order.
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.
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
The rating and checks come from
candidate_resume_scores. The result is read from the first
record that has one:
- 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.
- 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.
- The path: a landing on Resume Rejected.
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
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.
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_intervieworscheduling. Module groups name the module that records them - a type:
number,yes_no,choice,date(timezone-aware) ortext. 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
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.