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.
Overview
Once a candidate is added to a job, Tenzo decides when and how to reach out based on a stack of gates: stage, consent, business hours, cooldown, retry budget, and channel-specific guards (phone line type, ATS sync state). Every outbound attempt has to pass every gate. This page describes the rules. For the consent rules in isolation, see Candidate Consent. For the broader stage flow, see Candidate Stage Lifecycle.When Outreach Starts
A candidate becomes eligible for outreach when their tenzo_stage transitions toOUTREACH. Two paths get them there:
- Resume passes —
AWAITING_RESUME_REVIEW → OUTREACHafter the resume scorer accepts the candidate against the job’s requirements. - Recruiter override —
RESUME_REJECTED → OUTREACHwhen a recruiter manually clicks Progress to Interview. Any scheduled rejection email is cancelled at the same time.
OUTREACH, a PENDING call row is created (idempotently — if one already exists, nothing happens). The scheduled_datetime on that row is what the call dispatcher polls for.
A candidate added to a job who is already being contacted on another job at the same org never goes to
OUTREACH directly — they go to COOLING_DOWN first. See Cooldown below.Scheduling the First Attempt: Outreach Hours
The firstscheduled_datetime is computed from the org’s Outreach Hours settings and the candidate’s timezone.
- Timezone:
candidate.timezoneon the candidate row. The scheduling code itself does no inference at outreach time — it reads the field as-is and falls back toUS/Easternif empty. The field is populated at candidate intake in this order: (1) explicit value from the ATS/import, (2) zip-code geocoding, (3) phone area-code lookup. If all three fail at intake, the candidate stays untimezoned and is treated as Eastern at outreach time. - Weekday window: Configurable per org (default
08:00–21:00candidate-local). - Weekend window: Configurable per org.
call_on_weekendsis on by default and weekend hours default to the same window as weekdays. Ifcall_on_weekendsis off, weekend slots roll forward to Monday.
Initial Outreach Method
Each job has aninitial_outreach_method setting that biases the first contact:
| Setting | Behavior |
|---|---|
OUTREACH_HOURS (default) | First outreach only (is_first_outreach, gated by sms_status == NONE): inside outreach hours → call; outside → SMS + companion email. Lets late-evening adds get a same-day touch. Retries always wait for the next outreach window; SMS is never re-fired outside hours. |
CALL | Always place a call (subject to consent and line-type rules). |
SMS | Send an SMS as the first touch; a companion email is sent alongside. |
override_outreach_hours_for_initial_outreach=True, which fires the first contact immediately without rolling forward to business hours. This is used by SMS-first and Outreach-Hours flows so an evening sync still gets a same-day touch.
Web-call jobs
For jobs whoseCampaign.call_types.applicant_call_type (or sourced_call_type) is CallType.WEB, every PENDING Call row is created with web_call=True. The dispatcher short-circuits the normal phone path at the top of make_initiate_call_request: it calls invite_to_web_call instead, which creates a WebCallLink row and POSTs the internal /send_web_invite_email endpoint. The email template comes from the customer’s Cosmos script_info.webCallEmailBody / webCallEmailSubject and includes a candidate-specific link to the scheduling/start page.
The retry budget (retry_count, retry_hours) and outreach hours still apply — each retry resends a fresh invite email instead of dialing. Link expiration is governed by VideoInterviewLinkExpirationSettings (default: no expiration). When the candidate clicks the link and schedules, the stage transitions to AI_INTERVIEW_SCHEDULED and the retry loop stops.
Channel Selection at Send Time
When the dispatcher picks up aPENDING call, it re-evaluates channel right before sending:
LANDLINE, PAGER, VOICEMAIL, PREMIUM, SHARED_COST, FIXED_VOIP, or NON_FIXED_VOIP. Tenzo looks the number up against Twilio and caches the result for 30 days. If the lookup is missing or fails, the SMS is allowed through.
Consent gate. Before any channel is used, consent is checked. See Candidate Consent for the full matrix, but the short version:
all_ai_communications_consent=Falseblocks call and SMS entirely. Email is not gated by this flag — email proceeds based onemail_consentalone.sms_consent=Falseblocks SMS; the system falls back to email when possible.call_consent=Falseblocks calls.email_consent=Falseblocks email (no further fallback).
Retries and Cooldowns
Retry Budget
When a call ends without a conversation (voicemail, no-answer, busy), a newPENDING call is scheduled. The behavior comes from the job’s Call Retry Settings:
| Setting | Default | Meaning |
|---|---|---|
reschedule_on_voicemail | True | Master switch for voicemail retries and consent-gated retries. Turning it off disables both. |
retry_count | 5 | Maximum retry attempts after the first call. |
retry_hours | 26.0 | Spacing between retries (hours). |
retry_count is exhausted, the candidate’s stage transitions out of OUTREACH and no further calls are scheduled.
Consent-Gated Retry
If a call is cancelled at dispatch time because consent is missing (and the candidate can’t be reached via email fallback either), Tenzo schedules a consent-gated retry atretry_hours later — up to retry_count + 1 total attempts. This exists so that a consent flip during the retry window (e.g. the ATS sync adds opt-in, or the candidate texts back START) gets picked up.
Failure-Reschedule
A separate, short rescheduler kicks in when the call initiation itself fails (Retell rejects the request, phone-provider error, etc.). The call is bumped 10 minutes forward and re-bounded to outreach hours.Cooldown Across Jobs
When a candidate is added to a job, Tenzo checks if they’re already in motion on another job at the same org. If so, the new job’sCandidateCampaignInfo enters COOLING_DOWN instead of OUTREACH. They auto-resume once the other outreach concludes, or after 14 days they expire to COOLDOWN_EXPIRED with no contact.
Cooldown days are configurable per org by candidate source and call type:
| Source | Channel | Default |
|---|---|---|
| Sourced | Phone | 1 day |
| Sourced | Web | 1 day |
| Applicant | Phone | 1 day |
| Applicant | Web | 0 (disabled) |
What Stops Outreach
Outreach is halted — pending calls cancelled, no new ones created — when any of these become true for the candidate:Stage-based stops
If the candidate’s stage moves into any of the following, all non-terminal calls are markedNOT_INTERESTED and no new outreach is scheduled:
NOT_INTERESTED— candidate declined this job; can be re-engaged for other jobs.DO_NOT_CONTACT— candidate is on the org’s DNC list; blocks outreach on every job. See Do Not Contact below.FRAUD_REJECTEDDISPOSITIONED_IN_ATS(the ATS marked them rejected/withdrawn)USER_CANCELED(recruiter explicitly stopped outreach)FAILED_KNOCKOUTHUMAN_REVIEW_REQUESTEDAI_INTERVIEW_SCHEDULED(the candidate has scheduled a web call — typically by following an SMS invite link; phone outreach pauses until the web call lands)SKIP_OUTREACH(the dispatcher hit a gate that should prevent contact but isn’t a permanent terminal — e.g. an ATS-eligibility soft-block)COOLING_DOWN
INVALID_CONTACT is functionally a stop too, but it doesn’t sit in the blocking-stages list — it cancels only the call that surfaced the problem and is documented under Invalid contact and other dispatch gates below.
Consent-based stops
all_ai_communications_consent=False→ no calls, no SMS. Email is independent — it still goes through ifemail_consent=Trueand an email integration is connected.sms_consent,call_consent, andemail_consentare per-channel master gates. With all three false, outreach is cancelled entirely.
Retry exhaustion
Whenretry_count is reached without a successful conversation, the candidate exits OUTREACH and no more calls are scheduled.
Invalid contact and other dispatch gates
Beyond stage and consent, the dispatcher applies several operational gates at the moment aPENDING call comes due. These don’t move the candidate to a “stopped” stage in advance — they only fire when dispatch tries to send.
- Paused job. Every dispatcher query filters out calls whose campaign status is
PAUSED. While a job is paused, no candidate on that job is contacted, regardless of their scheduled time. Calls resume when the job is unpaused; their existingscheduled_datetimeis honored. - Missing or invalid phone. Phoneless candidates with email + email-consent get a single outreach email + a consent-gated retry, cancelled with
NO_PHONE_EMAIL_FALLBACK(analytics-distinct from the AI-comms-opt-out branch’sNO_AI_COMMUNICATIONS_CONSENT, but both share the same retry-budget filter). The shared body lives in_try_email_only_outreach; the two outer branches differ on their precondition and their failure-mode resolution. If email isn’t viable for a phoneless candidate (no email, no consent, orget_no_answer_email_taskno-op’d), the email-only branch is skipped/falls through — the downstream no-phone gate inmake_initiate_call_requestthen transitions the candidate toINVALID_CONTACT. Web-call jobs (Call.web_call=True) are excluded from this branch —invite_to_web_callhandles them with awebCallEmailinvite and doesn’t need a phone. - SMS invalid-number callback. When an SMS provider (Twilio or Telnyx) returns an invalid-number error code on send, the SMS failure policy transitions the candidate to
INVALID_CONTACT. This is a single-strike rule — one invalid-number response from the carrier halts SMS for that candidate. - SMS error threshold. Independent of invalid-number errors, repeated SMS failures against the same candidate-job pair (currently 10) disable further SMS attempts on that pair. This is a per-pair circuit breaker for transient provider issues.
- From-number resolution. A candidate’s call must be placed from a phone number registered to the org. If no usable from-number is available at dispatch time (no registered numbers, all numbers exhausted), the call fails and is rescheduled via the failure-reschedule path described above.
- ATS org-policy eligibility. For ATS-synced orgs, the dispatcher re-runs the ATS outreach eligibility check just before sending. If the ATS now classifies the candidate as ineligible (org-level reject/withdraw status, ATS-side DNC list, custom blocklists), the call is cancelled and the candidate transitions directly into a blocking stage — typically
DO_NOT_CONTACTorSKIP_OUTREACH, depending on the reason. This is separate from the candidate-leveldo_not_contactflag; it’s how org policy at the ATS reaches the dispatcher. - Concurrency cap. The dispatcher processes one outbound call at a time (
MAX_CONCURRENT_CALLS = 1). On orgs with a large backlog, this means aPENDINGcall that comes due during a busy minute may slip a few seconds pastscheduled_datetime. It is not user-tunable. - Stale-call cleanup. Long-pending calls on ATS-synced candidates are periodically checked against the ATS for fresh status. If the candidate has since been rejected or withdrawn in the ATS, the pending call is cancelled and the candidate moves to
DISPOSITIONED_IN_ATS.
Other Outreach Touches
The rest of this page is about the prospecting outbound path (call/SMS/companion email at the top of the funnel). A handful of other automated touches share the same gates but have their own schedulers, and a reader asking “why didn’t this email go out?” needs to know they exist:- Rejection email. Scheduled 24 hours after the candidate enters
RESUME_REJECTED, then rounded forward to the next outreach-hours slot in the candidate’s timezone.enable_rejection_emailsdefaults to off at the org level — most orgs don’t send them unless they’ve opted in. Subject to email consent; if the candidate transitions back toOUTREACHbefore the email fires, the scheduled send is cancelled. Sent byprocess_scheduled_rejection_emails. - Resume feedback email. Parallel flow to the rejection email but enabled by default. Same consent gate, same scheduler pattern (
process_scheduled_resume_feedback_emails). - Scheduling reminders. Once a candidate has agreed to schedule a call but hasn’t picked a time, the system re-pings them on a configurable cadence (
process_scheduling_reminders). These pings honor outreach hours and per-channel consent the same way prospecting touches do. - Meeting reminders. Pre-interview SMS reminders (24h and 1h ahead of a scheduled call), sent by
process_scheduled_meeting_reminders. Consent-gated; not subject to outreach hours because the meeting time itself is what’s being honored. - Video interview reminder emails. Email reminders before a self-scheduled web call, governed by
VideoInterviewReminderSettings(defaults:reminder_email_count=2,reminder_email_hours=24.0). Distinct from the SMS meeting reminders above — these fire for video interviews only and use the email channel.
Inbound Call Routing
Outbound is only half the picture: candidates can dial the phone numbers Tenzo uses to reach them. The Twilio/inbound_call webhook routes incoming calls in this order:
- Invalid/anonymous caller ID → reject.
- Permanent org phone number (DB-mapped to a campaign) → candidate-intake flow (collect name/email at the top of the call).
- Legacy hardcoded mapping (Dollar Tree).
- Resumable incomplete call matched by from/to pair → resume the existing session via the State Snapshot Pattern.
- Known caller with prior call → duplicate the prior call, start a new interview.
- No prior-call phone match → verification flow. Creates a
VerificationCallrow and routes to a websocket that asks the caller for an 8-character code, then their full name. On both validations, the inbound call is linked to the candidate row and transitions into the interview.
candidate_campaign_info.candidate_campaign_id[:8].upper() and surfaced to candidates via the auto-appended footer block in every outreach email rendered by send_virtual_interview_email (the email.footer.verification i18n string). Candidates with no usable phone-on-file can still convert by dialing in and reading back the code.
The inbound verification flow does not write the verified caller’s number back to the candidate row by default. The phoneless email-only outreach work in this branch adds that write (best-effort, only when the candidate’s field is empty) so subsequent dispatch attempts pick up the candidate as phone-eligible.
Do Not Contact
DNC is org-wide, not per-job. It’s implemented as two coupled pieces of state:Candidate.do_not_contact— boolean flag on the candidate row. Once true, no job at the org will reach out.Candidate.dnc_reason— why the flag was set:CANDIDATE_REQUESTED,SMS_STOP,ATS_EXCLUDED_STATUS, orLEGACY_UNKNOWN.tenzo_stage = DO_NOT_CONTACTon eachCandidateCampaignInfo— the per-job reflection of the flag. Only legal exit transition is toDISPOSITIONED_IN_ATS, so this stage is effectively terminal.
NOT_INTERESTED vs DO_NOT_CONTACT:
NOT_INTERESTED | DO_NOT_CONTACT | |
|---|---|---|
| Scope | This job only | All jobs at the org |
| Source | Candidate declined this role | Candidate asked to be removed entirely, or ATS marked them so |
| Re-engageable | Yes — recruiter can override or another job can re-contact | No — flag persists; the org-wide DNC list does not auto-clear |
| Legal exits | DISPOSITIONED_IN_ATS, SKIP_OUTREACH, USER_CANCELED | DISPOSITIONED_IN_ATS |
How candidates end up on DNC
-
Inbound SMS — candidate asks to be removed. During an SMS conversation, the LLM classifies the candidate’s intent. If they express a global opt-out (“don’t ever text me again”, “remove me from your system”), the SMS not-interested tool calls
execute_confirm_global_dnc, which setsdo_not_contact=Truewithdnc_reason=CANDIDATE_REQUESTEDand moves the SMS conversation state toDO_NOT_CONTACT. If the message reads as a this-job-only decline, the tool callsexecute_confirm_campaign_opt_outinstead — that’sNOT_INTERESTED, not DNC. -
SMS
STOPkeyword. A keyword-level STOP from the carrier (Twilio’s built-in unsubscribe handling, or the explicit STOP-detection path) sets the flag withdnc_reason=SMS_STOP. This is independent of LLM classification. -
Call result. If the candidate tells Morgan on a call to take them off the list, the same
execute_confirm_global_dncpath runs from the post-call processor (via the not-interested action), withdnc_reason=CANDIDATE_REQUESTED. -
ATS sync (provider-specific). Some ATSes have a “do not contact” or equivalent status that Tenzo mirrors:
- JobDiva — candidates with status “Unavailable Indefinitely” are pulled into the DNC list nightly with
dnc_reason=ATS_EXCLUDED_STATUS. If their status changes back later, this reason can be cleared. - Outreach eligibility mapping — any ATS provider whose eligibility check returns
OutreachBlockCategory.DO_NOT_CONTACTtriggers a transition to theDO_NOT_CONTACTstage at the time the candidate is added to a job.
- JobDiva — candidates with status “Unavailable Indefinitely” are pulled into the DNC list nightly with
-
Defense-in-depth at dispatch. Independent of how the flag was set, the call dispatcher re-checks
candidate.do_not_contactimmediately before placing each call. If true, everyPENDINGcall on that candidate is cancelled withCancellationReason.DNC_MATCHand the candidate’s stage on the current job transitions toDO_NOT_CONTACT. This guards against a stalePENDINGrow that was scheduled before the DNC flag flipped.
Setting
do_not_contact=True does not retroactively rewrite every existing CandidateCampaignInfo to DO_NOT_CONTACT. Other jobs flip when their next dispatch happens (or, for ATS-driven flips, when add-to-campaign next evaluates the candidate). Inbound SMS routing is short-circuited immediately because it reads the flag directly.Reasons that can be cleared vs reasons that stick
CANDIDATE_REQUESTEDandSMS_STOPare treated as the candidate’s explicit, persistent choice. ATS sync will not overwrite the flag toFalsewhen these reasons are present.ATS_EXCLUDED_STATUScan be cleared if the ATS status moves out of the excluded state.LEGACY_UNKNOWNexists for rows migrated from an earlier system without an explicit reason.
Related
- Candidate Consent — full consent matrix and channel fallback rules
- Candidate Stage Lifecycle — stage transitions and what each stage means
- Cooldown across jobs — cross-job cooldown behavior, expiration
- SMS Registration — A2P 10DLC and SMS compliance