1. When NLU beats DTMF
DTMF menus are still the right tool for short, closed-option menus (“press 1 to hear your balance”). Reach for NLU when any of these apply:- Wide or flat option sets. Once the menu runs past 3–4 options, DTMF abandonment climbs. One spoken utterance beats “press 1, then 3, then 2”.
- Open-ended requests. DTMF trees can only encode options you anticipated. NLU routes utterances you never planned a branch for — “someone charged me twice” maps to
billingwithout a dedicated key. - Accessibility and mobile callers. Spoken requests beat keypad entry for callers on hands-free devices.
- Continuous improvement. Fallback analytics show the utterances that failed to match, so you add intents for real caller language instead of designing menus in the dark.
2. Model your intents
An intent is a named bucket (object fields). Three rules of thumb:- Name it like a handle. Lowercase alphanumeric plus
-/_, max 64 chars, unique per tenant:billing,sales-eu,tech_support. You branch on this slug in your flow, so keep it stable — the API id (ivri_*) survives renames, the slug is what your code reads. - Write a rich description. The classifier evaluates the utterance against the description text, not the slug. “Billing” matches little; “Caller has a question about an invoice, charge, payment, or refund” matches a lot. Name the real vocabulary callers use.
- Analytics-only intents are valid. Leave both route fields null and the match is recorded without routing — useful for sizing a new queue before you staff it, or for topic reporting. See section 3.
3. Route to queues or AI agents
Each intent routes to exactly one destination:
The two fields are mutually exclusive — set both and create/update returns
422 VALIDATION_ERROR. Set neither and you get an analytics-only intent. To switch an intent’s destination, PATCH one field to the new value and the other to null.
4. Test utterances before going live
POST /api/v1/voice/ivr-intents/test classifies an utterance against your active intents without placing a call. Use it as a pre-publish gate: candidate descriptions that can’t beat the threshold stay out of production.
The contract: a match commits at confidence ≥ 0.7; below that, matched_intent_id is null and fallback_reason is one of:
The response also carries the model’s
reasoning, so a failed match tells you which direction to move the description. Note: a 422 VALIDATION_ERROR here only means the request shape was wrong (missing utterance or over 500 chars) — a clean no_match runs the classifier anyway so you see why an empty active set can’t route.
5. Worked example: support triage
Build a three-way triage —sales, billing, support.
cURL
billing → que_billing and support → que_support. If your support destination is an AI agent, send target_agent_id: "agt_..." instead — the classifier doesn’t care which target type sits behind the slug.
Now test candidate utterances:
cURL
6. Apply the intents in an IVR flow
A flow that speaks, gathers one utterance, classifies it, and branches:speechInput node; edges from it branch by intent slug, and unmatched utterances take the default edge. The voice guides — the flow builder walkthrough for graph validation, simulation, and publishing, and the voice quickstart for the inbound attach path — run in parallel with the visual Flow Builder, which shares the same node palette. Use either editing surface; the published graph is what the runtime walks.
Simulate the fallback path too — send an empty turn list and confirm the caller lands on voicemail rather than hanging in the classifier node.
7. Multilingual calls
Setlanguage per classification to a BCP-47 tag (en-US, tr, es-419, …). The tag goes into the classifier prompt, so it changes how the model weighs the utterance — pass the real caller locale rather than defaulting to en. If you serve multilingual queues, re-run your section-5 fixture utterances per language; a description that reads well in English can still be thin for another locale. Keep the descriptions themselves in one language and let the tag do the work — mixing languages inside the same description hurts all of them.
8. Observability and hygiene
- Audit logs. Every
create/update/delete/testwrites an audit entry (voice.ivr_intent.<verb>), so you can see who changed which description and when — trace a routing regression back to the edit. - Prompt hygiene. The classifier builds its prompt from the active set. Deactivate rather than delete during experiments: deactivation drops the intent out of classification without erasing its edit history, and reactivation is one PATCH of
active: true. - Deactivate noisy intents. An intent that keeps matching the wrong utterances skews live routing — drop it out of the active set while you rewrite the description.
- Watch your base rate. Frequent
ambiguousfallback on intent pairs (salesvsbilling,supportvsreturns) usually means both descriptions phrase the same middle ground — differentiate by the decisive word (invoice vs pricing, broken vs refund).
9. Troubleshooting
Low-confidence clusters. If tests against one intent keep landing in 0.4–0.69, the description is thin. Add the callers’ literal vocabulary (“invoice”, “charged”, “refund”, “payment plan”) and retest — a 500-char budget is room for a lot of synonyms. Ambiguous ties. Split by the decisive feature — put “upgrade / pricing / new order” intosales and “invoice / charge / refund” into billing — then rerun the utterances that tied.
Empty active set. Every intent deleted or active: false returns fallback_reason: "no_match" on every classification — if the dashboard route seems frozen but live calls keep hitting the fallback edge, check for an all-inactive set. That state is valid for analytics but routes nothing.
Duplicate-name conflicts. 409 IVR_INTENT_NAME_CONFLICT on create, or a 409 raced against another writer. GET / first and reuse one slug set across your routing code and flow edges.