Confidence-gated routing
Use confidence as a second axis — the answer says what, confidence says whether to act — with a minimum confidence per question and per-action AND rules. Built on the Voice Banking template.
Use confidence as a second axis: the answer tells you what, confidence tells you whether to act on it. Some actions are riskier than others, so they need more certainty before your system acts on its own.
Benefits: reliability and safety. TypeSafe's guide: Confidence-gated routing.
Why it works
Jev is trained to return calibrated probabilities: across many answers, those given about 80% should be right about 80% of the time. Every answer carries a confidence between 0 and 1 derived from that distribution — 1 when all the probability sits on one answer, 0 when it is spread evenly. "I'm not sure" becomes a number your system can route on, instead of a confident wrong answer.
A useful starting point is three paths:
| Confidence | Behavior |
|---|---|
| High | Act automatically. |
| Medium | Proceed with caution: ask the user to confirm, flag for review or gather more information. |
| Low | Don't act: route to a person, ask for clarification or fall back to another system. |
Where the boundaries sit depends on the stakes: thresholds scale with risk. Reading a balance at 60% confidence is fine — the worst case is a wrong screen. Approving a transfer is not.
When to use it
Any action with consequences: payments, account changes, deletions, messages sent on someone's behalf, moderation that removes content. Pair it with intent routing: the intent picks the handler, confidence decides whether the handler may act alone.
How it maps to Dcision
| In TypeSafe's guide | In Dcision |
|---|---|
if action.confidence < 0.6: route_to_support_agent() | minConfidence: 0.6 on the question — below it, the decision returns its fallback action (escalate) with action_reason.type = "low_confidence" |
elif choice == "approve_transfer" and confidence > 0.85: approve() | A rule per risky action: intent = approve_transfer AND intent confidence below 0.9 → escalate |
else: ask_user_to_confirm() | Your code maps escalate from that rule to "ask the user to confirm" |
| A catch-all option | The reserved other option, which also returns the fallback action |
The rules run first, then the other check, then minConfidence — see How the action is computed.
Build it
In the app
Open Templates and choose Voice Banking Commands (confidence-gated). Name the decision Voice Banking so its slug is voice-banking, then Create decision.
The State is Text: your speech-to-text transcript of the command.
The only question, intent, is a choice: check_balance (read-only), transfer_money, approve_transfer, block_card and the reserved other. Its Minimum confidence is on, at 60% — the floor for every action.
In Policies, one rule per risky action, using the question's confidence as a second condition:
- IF
intent· answer · = ·approve_transfer, + ANDintent· confidence · < · 0.9 — THENescalate. - IF
intent· answer · = ·transfer_money, + ANDintent· confidence · < · 0.8 — THENescalate.
In Engine & runtime, keep the Fallback action at escalate. For a voice channel that must always answer, consider If the engine fails → Answer with the fallback action (see Decision settings). Test, then Deploy v1.
The decision schema
{
"stateSchema": { "kind": "text" },
"questions": [
{
"key": "intent",
"type": "choice",
"instructions": "What does the customer want to do with their account?",
"minConfidence": 0.6,
"options": [
{ "value": "check_balance", "description": "hear the balance or recent transactions (read-only)" },
{ "value": "transfer_money", "description": "start a new transfer to someone" },
{ "value": "approve_transfer", "description": "confirm a transfer that is waiting for approval" },
{ "value": "block_card", "description": "block or freeze a card" }
]
}
],
"policies": [
{
"field": "intent",
"on": "output",
"operator": "eq",
"value": "approve_transfer",
"and": [{ "field": "intent", "on": "confidence", "operator": "lt", "value": 0.9 }],
"action": "escalate"
},
{
"field": "intent",
"on": "output",
"operator": "eq",
"value": "transfer_money",
"and": [{ "field": "intent", "on": "confidence", "operator": "lt", "value": 0.8 }],
"action": "escalate"
}
],
"settings": { "fallbackAction": "escalate" }
}Call it
The state is the transcript, as a string:
curl -X POST https://api.dcision.io/v1/decisions/voice-banking \
-H "Authorization: Bearer $DCISION_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "state": "Yeah, go ahead and approve that payment to my landlord." }'{
"decision_id": "dec_Wb3Kq9Tz1Lm7Xv5Rn2Hd",
"execution_id": "exec_Pz8Nc4Wq1Ht6Ks3Lm9Vb",
"schema": "voice-banking",
"version": 1,
"result": { "intent": "approve_transfer" },
"confidence": { "intent": 0.8125 },
"action": "escalate",
"action_reason": { "type": "policy", "rule": 0 },
"metrics": {
"latency_ms": 248,
"engine": "jev",
"model": "jev-1.13.0",
"estimated_cost_usd": 0.00001008,
"input_tokens": 240,
"output_tokens": 14
}
}The intent is clear enough to pass the 60% floor, but approving a transfer needs 90%: rule 0 escalates. The same decision on other commands:
| Command | result.intent | confidence.intent | action | action_reason |
|---|---|---|---|---|
| "Yeah, go ahead and approve that payment to my landlord." | approve_transfer | 0.8125 | escalate | { "type": "policy", "rule": 0 } |
| "What's my balance?" | check_balance | 0.95 | continue | { "type": "default" } |
| "Uh, the thing from before, with the money" | check_balance | 0.4 | escalate | { "type": "low_confidence", "question": "intent", "confidence": 0.4, "minConfidence": 0.6 } |
| "Change my mailing address" | other | 0.9 | escalate | { "type": "other_option", "question": "intent" } |
Act on it
action_reason tells the two kinds of escalate apart: a rule fired because a risky action wasn't certain enough — confirm it — or the decision didn't understand — hand over:
const decision = await response.json();
const { action, action_reason: reason, result } = decision;
if (action === "continue") return perform(result.intent, account);
if (reason.type === "policy") {
// A risky intent below its threshold: verify instead of acting.
return voice.ask(`Just to confirm: you want to ${describe(result.intent)}, is that right?`);
}
return transferToAgent(call, { reason }); // low_confidence, other_option or engine_errorTune it
- Start conservative, then measure. Pull recent runs with
GET /v1/executionsor the Executions screen, compareconfidencewith what really happened, and move each threshold on its own. - One rule per risk level. A decision has up to 50 rules with up to 5
andconditions each — enough for a threshold per action, per customer tier or per amount band computed in your code. - Re-check thresholds when options change. Choice confidence measures how far the top option sits above an even split between all options,
otherincluded: adding or removing options moves the scale. - Probability questions have a different confidence. For a yes/no probability, confidence is
|2p − 1|: aminConfidenceof 0.6 flags everypbetween 0.2 and 0.8 — see Confidence and probabilities. - Keep errors on the same path. With
onEngineError: "fallback", an engine outage answersescalatewithaction_reason.type = "engine_error"instead of an HTTP error — handled above by the last line.
Related: Handling low confidence and other · Intent routing
Speculative fan-out
Ask every question a flow might need in one call — Jev answers them in parallel — and let AND rules and your code decide which answers matter. Built on the Support Ticket Triage template.
Composite scoring
Break a judgment into atomic scores and combine them with weights you control — composites computed by Dcision and usable in policies. Built on the Resume Screening template.