Skip to main content
Restricted Access: This documentation is only accessible to @tenzo.ai and @salv.ai email addresses.
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. 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.

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:
  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

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_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
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).