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
| Condition | DISPOSITION | SUB_DISPOSITION | CALLING_REMARKS |
|---|
| RNR | RNR | RNR | Customer picked up but did not respond. |
| Voicemail | IVR | MAIN_MESSAGE | Voicemail or automated system detected. |
| Timeout | Not Connected | DSCN_PARTIAL | Call exceeded maximum duration. |
| Error | Failed | fallback | Call ended due to a system error. |
| No customer message | RNR | RNR | (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:
- 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.
- 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”.
- Sends the formatted transcript as a separate user message (
Call transcript:\n\n...), not interpolated into the prompt.
Token limits (per provider):
| Provider | Limit | Field |
|---|
| Explicit-cache LLM providers | 8192 | max_output_tokens |
| Request-hint LLM provider | 4096 | max_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).
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.
Each post-call and QC usage entry carries a cache dict describing what happened:
status | Meaning |
|---|
disabled | Caching not enabled for this call. |
hit | Reused an existing cache (from registry or Redis). |
created | A new CachedContent was created this call. |
fallback | Cache creation failed; ran without caching. |
ineligible | Enabled but the static prompt was empty. |
stale_retry | Cached generate failed; retried without the (stale) cache. |
unsupported_provider | Provider 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
| Value | Meaning |
|---|
bot | Bot called end_call, spoke closure phrase, or dead air timeout |
customer | Customer hung up |
voicemail | Voicemail detected |
RNR | Customer picked up but stayed silent (dead air) |
outside_hours | Call rejected — bot outside active hours |
no_answer | SIP 408/480 — callee didn’t answer (outbound only) |
rejected | SIP 486/603 — callee rejected (outbound only) |
timeout | Max call duration exceeded |
error | Pipeline startup failure or unhandled exception |
transfer_to_agent | Call 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.