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:

ConfidenceBehavior
HighAct automatically.
MediumProceed with caution: ask the user to confirm, flag for review or gather more information.
LowDon'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 guideIn 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 optionThe 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:

  1. IF intent · answer · = · approve_transfer, + AND intent · confidence · < · 0.9 — THEN escalate.
  2. IF intent · answer · = · transfer_money, + AND intent · confidence · < · 0.8 — THEN escalate.

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." }'
200 OK
{
  "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:

Commandresult.intentconfidence.intentactionaction_reason
"Yeah, go ahead and approve that payment to my landlord."approve_transfer0.8125escalate{ "type": "policy", "rule": 0 }
"What's my balance?"check_balance0.95continue{ "type": "default" }
"Uh, the thing from before, with the money"check_balance0.4escalate{ "type": "low_confidence", "question": "intent", "confidence": 0.4, "minConfidence": 0.6 }
"Change my mailing address"other0.9escalate{ "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_error

Tune it

  • Start conservative, then measure. Pull recent runs with GET /v1/executions or the Executions screen, compare confidence with 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 and conditions 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, other included: adding or removing options moves the scale.
  • Probability questions have a different confidence. For a yes/no probability, confidence is |2p − 1|: a minConfidence of 0.6 flags every p between 0.2 and 0.8 — see Confidence and probabilities.
  • Keep errors on the same path. With onEngineError: "fallback", an engine outage answers escalate with action_reason.type = "engine_error" instead of an HTTP error — handled above by the last line.

Related: Handling low confidence and other · Intent routing

On this page