1. Intent vs slot: what slot-filling adds
An intent routes the call; a slot fills in the record. With intent-only routing, “I want to report an outage at 123 Main Street” lands in the right queue and every detail is re-asked by a human. With a DTMF tree, each detail costs a keypad branch — fine for a 4-digit PIN, unusable for a street address. Slot-filling sits between them:- One intent, many values. The caller’s utterance is mined for several structured values at once — account number, date, amount, address — instead of one regex per prompt that you write and maintain yourself.
- Validation in the loop, not after it. Each candidate value is checked against its slot type (digit count, calendar validity, closed value set) before it is accepted. A hallucinated or mis-heard value never lands in your record; the caller is reprompted with the reason instead.
- Relative speech becomes absolute data. “It started yesterday” is resolved to a full calendar date; “$1,234.50” and “1234.5” normalise to the same amount.
2. Define an intent and its required slots
Two surfaces, one flow. The intent is created on the IVR intents endpoint (POST /api/v1/voice/ivr-intents) exactly as in the routing guide — a slug, a rich description, and a destination:
cURL
POST /api/v1/voice/ivr-slots/extract, so there is nothing to migrate or pre-provision — your flow threads the spec and the accumulating values through each turn:
A few validators worth designing prompts around:
date accepts only an absolute calendar date (the extractor resolves “yesterday” for you, but an unresolvable “sometime last week” returns null and the slot stays open), amount normalises to at most two decimal places, and account_number strips the pauses STT inserts between spoken digits. Extraction runs one turn per request — send { "utterance", "slots", "filled", "language" } and receive per-slot status, the merged filled map, the next reprompt to play, and complete: true once every required slot holds a valid value.
3. Prompt, confirm, reprompt
The extract endpoint is stateless by design: it validates one utterance and hands back the accumulatedfilled map. Your flow owns the loop and the attempt counter. The turn shape:
- Play
next_prompt.message(or your opening prompt on the first turn). - Capture the caller’s speech and call
/ivr-slots/extractwith the priorfilledmap. - If a slot came back
invalid, its reprompt is prefixed with the rejection reason (“Account number is too short (need at least 10). Please repeat…”) — play it verbatim; it tells the caller why. - Loop until
completeis true or your retry budget is exhausted.
target_queue_id or target_agent_id with whatever slots did fill attached, so the agent never re-asks for values you already hold. Escalating with partial slots is almost always better than escalating empty-handed.
Confirmation turns. For values that are expensive to get wrong, confirm by reading the normalised value back and capturing the answer as a yes/no enum slot — that is what callback_ok above models, and the same shape works for confirming an account number: “I have account number ending 4 0 2 1 — is that correct?” A “no” means you delete that key from filled before the next extract call so the slot reopens; the validator never re-accepts a value you keep passing in as already filled. Keep confirmations to one turn each and only for slots where a mistake has a real cost — every extra confirmation turn is measured abandonment.
4. Bias recognition with NLU hints
Numerals and addresses are where speech recognisers stumble, because your callers’ vocabulary is not the recogniser’s default vocabulary. The NLU hints catalogue (/api/v1/voice/ivr-nlu-hints) is where you seed example phrasings per intent; POST /api/v1/voice/ivr-nlu-hints/build-hints flattens the catalogue into the deduplicated, normalised hint list a speechInput node’s recogniser consumes — call it to preview exactly what the recogniser will see.
Hinting the 10-digit account number and the street-address grammar for our example:
cURL
cURL
- Hints are example phrasings, not regexes — write the sentences callers actually say, including the filler words (“my account number is…”) the recogniser should look past.
- Every utterance is normalised (lowercased, punctuation folded to spaces) and deduplicated; the same phrase in two intents is kept once, first intent wins.
build-hintsreports adroppedcount for unusable entries and a per-intent breakdown so you can audit what survived. - Limits: 64 intents per catalogue, 16 utterances per intent, 100 characters per utterance, 256 hints returned per build. Spend them on the slots that actually mishear — digits and addresses — not on greetings.
- Hints bias recognition; they do not validate. A hinted phrase that misfires still has to pass the slot’s deterministic validator (section 7), so hints widen what the recogniser hears while
minLength/maxLength/enumValueskeep the accepted set narrow.
5. Test collection end to end
Before the intent touches a live route, run the same harness you would for routing, extended down to the slots:- Routing first.
POST /api/v1/voice/ivr-intents/testwith your fixture utterances — “the power is out on my street”, “no service since this morning” — and confirmreport_outagewins at confidence ≥ 0.7 against your other active intents, per the routing guide’s threshold contract. - Turn-by-turn extraction. Script
/ivr-slots/extractcalls the way the flow will: first turn with an emptyfilled, each following turn passing the returnedfilledmap plus the next utterance. Assert the normalised values (account_numberis digits-only and exactly 10 long,ticket_dateis ISOYYYY-MM-DD), assert an invalid turn returnsstatus: "invalid"with a reason and a reprompt, and assertcompleteflips to true only when all four required slots hold. - The unhappy paths. Feed a 9-digit account number, a “February 30th” date, and a mumbled “maybe” against the yes/no slot — each must reject with a reprompt, never silently fill.
- The published graph. Run the flow through the simulator as in Build your first IVR flow: the escalation edge on retry exhaustion, the fallback edge when the intent never matches, and the happy path landing on the downstream node from section 6. Only then attach the intent to the live call route.
complete vs incomplete), so your fixture runs leave a trail you can diff against live traffic once the intake ships.
6. Where the filled slots land
When the collection loop completes, each filled slot is posted to the flow as a variable namedslot_<name> and exposed in the conversation context, using the same {{…}} templating the secure payment capture guide uses for captured values. A downstream node consumes them directly — for example, a playAudio node that confirms the logged ticket:
service_address, account_number, ticket_date and callback_ok already normalised — digits joined, date absolute, yes/no in your casing — with no parsing of raw transcripts on your side. Where the next step depends on a value, branch by edge rather than by expression: two edges out of the completing node, one taken when slot_callback_ok is yes, mirror how a speechInput node branches on intent slugs in the routing guide.
7. Failure modes
Slot never fills (bad hint grammar). If the recogniser never hears what the extractor can find, the slot cyclesmissing → reprompt until your retry budget escalates. Check build-hints first: a high dropped count means utterances exceeded the 100-character cap or normalised to nothing, and punctuation-heavy hints (“123 Main St.”) arrive folded to spaces — write hints the way speech sounds. Then check the extract turn’s reasoning: it tells you whether the extractor saw nothing in the utterance or saw a value the validator rejected.
Hints match too broadly (false accepts). A hint list seeded with short fragments (“yes”, “4 0 0”) makes the recogniser hear slot values in unrelated speech. The defense is the validator, not the hint: pin account_number to minLength: 10, maxLength: 10, keep enumValues closed, and treat description on the slot as the narrow grammar. A false accept then surfaces as a one-turn invalid reprompt instead of a corrupted record — and the confirmation turn (section 3) is the last line of defense for the values that matter.
Confirm-loop abandonment. Every confirmation and reprompt turn costs callers; long digit dictation on a mobile keypad is where most hang up. Keep required slots to what the downstream system truly needs, mark nice-to-have fields required: false so they never block complete, offer the DTMF fallback edge for digit-heavy slots, and always keep the escalation edge to a staffed queue or AI agent reachable when retries exhaust — with the partial filled map attached.