The double gate
Callbacks only schedule when both gates are satisfied:| Gate | Field | Owner |
|---|---|---|
| Bot toggle | bot.callback_prompt_injection | Bot config |
| Campaign flag | campaign.config.callback_detection.enabled | Campaign config |
_callback_detection_enabled + callback_prompt_injection). The runtime config exposes callback_detection_enabled so CXB Core can run dedicated callback extraction. See Conversation policy → callback prompt injection.
Extraction
extract_callback_request(analysis) reads only the standard analysis keys (no keyword scraping):
| Output | From analysis key |
|---|---|
requested (bool) | callback_requested |
preferred_time_text | callback_preferred_time_text |
reason | callback_reason |
confidence (float) | callback_confidence |
should_schedule_callback(campaign, analysis) = campaign gate AND requested.
Time resolution
resolve_callback_time(preferred_time_text, requested_at, campaign) converts the customer’s spoken timing into a concrete UTC datetime, in the campaign timezone (config.time_window.timezone, default Asia/Kolkata). Resolution order:
- Relative offset —
_relative_deltaparses “5 min”, “2 hours”, “aadhe ghante”/“half an hour” →requested_at + delta. - Daypart —
subah/morning→ 10:00,shaam/evening→ 18:00, with day offset markerskal/tomorrow(+1) andparso/day after(+2). - Day offset only → next day(s) at 10:00.
- Fallback — no clear time →
requested_at + retry_delay_minutes, with reasonNO_CLEAR_TIME_REASON.
_clip_to_window); a time outside hours is pushed to the next window start with OUTSIDE_WINDOW_REASON.
Persistence
Inprocess_campaign_call_result, when scheduling wins (and the campaign was not manually stopped, and reschedule count < MAX_CALLBACK_RESCHEDULES = 5), the contact is updated atomically (matched on the _processing lock):
- Status →
callback_scheduled,next_retry_at= resolved time. - A
callbacksub-doc:active,requested,status: scheduled,sequence,requested_at,scheduled_at,preferred_time_text,reason,confidence,source_session_id,source_attempt,exceeds_max_attempts,fallback_reason. - A new entry is pushed onto
callback_history(withcallback_attempt = current_attempt + 1). - When rescheduling from a prior callback, the previous
callback_historyentry is closed (completed/exhausted) via anarray_filtersupdate.
callback_requested + callback_scheduled; rescheduling from an existing callback also nets callback_completed/callback_connected so pending counts stay balanced.
Callback scheduling takes priority over normal retry logic — a requested callback supersedes the standard
redial_rules retry decision for that contact.Stopping callbacks
stop_campaign cancels scheduled callbacks: contacts in callback_scheduled go to manual_stopped with callback.status: cancelled (and open callback_history entries closed). Lease priority in CXB Dialler dials callbacks first. The GET /{id}/calls?callback= filter supports requested / pending / completed / cancelled views.
Operational checks
| Symptom | First place to check |
|---|---|
| Callbacks never scheduled | Both gates (callback_prompt_injection + campaign callback_detection.enabled). |
| LLM didn’t emit callback keys | Prompt injection — the analysis prompt must already lack callback_requested/callback_preferred_time_text for injection to apply. |
| Wrong callback time | preferred_time_text parsing + campaign timezone + window clipping. |
| Callback loops | MAX_CALLBACK_RESCHEDULES = 5 cap. |
| Callbacks dialled out of hours | _clip_to_window and the campaign time_window. |
Related docs
Operations: Callbacks
Operator-facing callback behaviour and tracking.
Campaign API
Campaign lifecycle, stats, and result reconciliation.
Conversation policy
Callback prompt injection into the analysis prompt.
CXB Core post-call
The dedicated callback extraction LLM call.