After every call ends, CXB Core runs post-call processing: auto-disposition rules, optional LLM analysis, optional QC checks, and durable result delivery to CXB API. All call routes share the same post-call logic.

Auto dispositions

Calls that meet certain conditions skip LLM analysis and receive an automatic disposition.

Skip conditions

LLM analysis is skipped if any of these are true (handled in the Core service’s shared post-call logic):
  • disconnected_by is error or voicemail — skipped unconditionally
  • disconnected_by is timeout or RNR and the customer never spoke (has_customer_message = false)
  • Call duration is less than 5 seconds
  • Customer never spoke (has_customer_message = false)
timeout and RNR are only skipped when the customer never spoke. A timeout or RNR call where the customer did speak is still analyzed — for example, a long conversation that hit the max-duration cap.

Default dispositions

ConditionDISPOSITIONSUB_DISPOSITIONCALLING_REMARKS
RNRRNRRNRCustomer picked up but did not respond.
VoicemailIVRMAIN_MESSAGEVoicemail or automated system detected.
TimeoutNot ConnectedDSCN_PARTIALCall exceeded maximum duration.
ErrorFailedfallbackCall ended due to a system error.
No customer messageRNRRNR(uses RNR disposition)
no_customer_message triggers when the customer never spoke, regardless of who disconnected or call duration. It only checks has_customer_message, not duration < 5.

Per-bot overrides

Set auto_dispositions in bot config to override defaults:
{
  "auto_dispositions": {
    "rnr": {
      "DISPOSITION": "No Response",
      "SUB_DISPOSITION": "SILENT",
      "CALLING_REMARKS": "Customer was silent throughout."
    },
    "voicemail": {
      "DISPOSITION": "Voicemail",
      "SUB_DISPOSITION": "VM_DETECTED"
    }
  }
}
Keys: rnr, voicemail, timeout, error, no_customer_message. Only DISPOSITION is required — other fields fall back to hardcoded defaults.

LLM post-call analysis

When a call qualifies for analysis (none of the skip conditions are met), CXB Core sends the transcript to the LLM with the bot’s post_call_analysis_prompt. The prompt defines what to extract and the expected JSON schema. The LLM response is stored as a raw dict in call results — CXB Core does not validate or transform the schema.

How the prompt is assembled

The bot’s post_call_analysis_prompt is not a template with {transcript} or {timezone} placeholders. Instead, the post-call analysis step in the Core service:
  1. Embeds the configured prompt into a hardcoded base template via the single {user_prompt} placeholder. The base template adds a JSON-only output instruction and hardship/non-listening guidance.
  2. Prepends a date/time context block (current date, time, and month in the bot’s timezone) so the LLM can resolve relative dates such as “25 tak”, “kal”, or “5 din mein”.
  3. Sends the formatted transcript as a separate user message (Call transcript:\n\n...), not interpolated into the prompt.
Token limits (per provider):
ProviderLimitField
Explicit-cache LLM providers8192max_output_tokens
Request-hint LLM provider4096max_tokens
max_output_tokens = 2048 applies only to the separate callback-extraction call (below), not to analysis or QC.

QC analysis

If qc_prompt is set in bot config, a separate LLM call runs quality checks against the transcript. Same mechanics as post-call analysis — the configured criteria are embedded via {user_prompt} into a QC base template that enforces a fixed score schema (empathy_score, compliance_score, overall_score, remarks), the transcript is sent as a separate user message, and the raw dict is stored in results. Same token limits as analysis (8192 for the explicit-cache LLM providers, 4096 for the request-hint LLM provider).

Callback extraction

When callback_detection_enabled is set in bot config, CXB Core can run a dedicated LLM call to detect whether the customer asked to be called back later (the callback-extraction step in the Core service’s post-call processor).
  • Gated twice: it runs only when callback_detection_enabled is true and the main post-call analysis returned callback_requested = true.
  • Dedicated multilingual prompt: focuses on intent (Hindi, English, Hinglish, Tamil, Telugu, Bengali, Marathi, and others), not specific keywords. Treats a bot promise like “I’ll call you back in X minutes” as proof a callback was requested.
  • Populates callback_requested, callback_preferred_time_text (customer’s exact words about timing), callback_reason, and callback_confidence (0.0–1.0) on the post-call analysis dict.
  • Uses max_output_tokens = 2048 and runs with caching disabled (cache_namespace = "callback_extraction").
  • Its usage is recorded as a separate post_call usage entry with processor = "callback_extraction".

Post-call context caching

For the explicit-cache LLM providers, the static system prompt for analysis and QC can be served from an explicit CachedContent to cut input-token cost across calls. Controlled by bot config post_call_cache_enabled and post_call_cache_version (and the nested post_call_cache dict, which can override per-namespace analysis_version/qc_version).
  • What is cached: the static base + configured prompt (without the per-call date/time block). The date/time block is sent as dynamic context alongside the transcript so the cache stays reusable.
  • Registry: a bounded in-process LRU registry plus a Redis layer (cxbcore:post_call_cache: keys) shares cache names across workers. Entries proactively refresh after ~90% of TTL.
  • Version-based invalidation: the cache key includes post_call_cache_version; bump the version to force fresh CachedContent rather than deleting caches. Old caches age out by TTL.
  • Request-hint LLM provider: post-call context caching is not supported; the usage cache status is reported as unsupported_provider.
See Caching for the full design, including the live-prompt cache and the in-process registry lifecycle.

Cache metadata on usage entries

Each post-call and QC usage entry carries a cache dict describing what happened:
statusMeaning
disabledCaching not enabled for this call.
hitReused an existing cache (from registry or Redis).
createdA new CachedContent was created this call.
fallbackCache creation failed; ran without caching.
ineligibleEnabled but the static prompt was empty.
stale_retryCached generate failed; retried without the (stale) cache.
unsupported_providerProvider does not support post-call caching (the request-hint LLM provider).
Usage entries also report cache_read_input_tokens (cached tokens reused) and cache_creation_input_tokens (tokens written when a cache was created).

Results webhook

After processing completes, CXB Core writes the final payload to the result outbox and sends the full call results to CXB API:
POST {webhook_url}
X-CXB-Core-Secret: {secret}
Content-Type: application/json
The webhook_url comes from bot config. See API contracts for the full payload structure. If CXB API is temporarily unavailable, the outbox retries in the background. This protects against transient delivery failures, but the outbox directory must be persistent on production hosts.

disconnected_by values

ValueMeaning
botBot called end_call, spoke closure phrase, or dead air timeout
customerCustomer hung up
voicemailVoicemail detected
RNRCustomer picked up but stayed silent (dead air)
outside_hoursCall rejected — bot outside active hours
no_answerSIP 408/480 — callee didn’t answer (outbound only)
rejectedSIP 486/603 — callee rejected (outbound only)
timeoutMax call duration exceeded
errorPipeline startup failure or unhandled exception
transfer_to_agentCall handed off to a live human via Agent Desk (set on successful enqueue)
When an auto-disposition is applied to a skipped call and a sip_dial_failed event with a reason is present in the call events, that reason overrides the disposition’s CALLING_REMARKS. This is handled in the Core service’s shared post-call logic.

Transfer state

When a call was transferred, CXB Core derives native transfer-state fields from the pipeline event stream and adds them to the call results: was_transferred, transfer_destination, transfer_target, transfer_reason, transfer_method, transfer_at, and transfer_failed_reason. See Transfer and escalation for how these are derived and what each field means.