Intent routing

Classify each request in one fast call and route it to the cheapest capable handler — code, a specialist LLM or a person. Built on the Customer Service Router template.

Not every request needs the same handler. Some are answered by a database lookup, some need an LLM with domain-specific context, some need a person. Put Dcision in front of all of them as a fast, cheap classifier: classify first, then route — and only pay for the expensive handler when the request actually needs it.

Benefits: cost and speed. TypeSafe's guide: Intent routing.

Why it works

  • A choice is a closed set. The answer is always one of your intents — never free text to parse — with a probability for every option.
  • "None of these" is an answer. The reserved other option lets the engine say a request fits no intent instead of forcing the least wrong one; Dcision then returns the fallback action.
  • One call answers the routing and the risk. Ask for the intent and its complexity together, in parallel.
  • The expensive part runs last. Lookups go straight to code, without any LLM; specialist models get only their own requests, with their own context.

When to use it

Inbound messages and tickets, chat and voice assistants, an agent choosing its first tool, any queue where requests differ in what they need — before you pay for an LLM call.

How it maps to Dcision

In TypeSafe's guideIn Dcision
A choice for the intentA choice question, plus the reserved other
A score for complexityA score question; its weighted level (scores) is usable in rules
if intent.confidence < 0.5: humanminConfidence on the intent question, or a rule on its confidence
complaint → human when complex or unsureRules that escalate the cases a person must see
handle_order_status(), handle_with_llm(…)Your code routes on result.intent when the action is continue

Build it

In the app

Open Templates and choose Customer Service Router (intent routing). Name the decision Customer Service Router so its slug is customer-service-router, then Create decision.

The State is Text: the customer's message.

Two questions, answered in the same call:

QuestionTypeAnswers
intentchoiceorder_status (answered by a database lookup), billing_question, technical_issue, complaint, other
complexityscoresimple, moderate, complex

In Policies, escalate what a person must see:

  1. IF intent · answer · = · complaint — THEN escalate.
  2. IF intent · answer · = · technical_issue, + AND complexity · weighted level · ≥ · 2.5 — THEN escalate.
  3. IF complexity · confidence · < · 0.5 — THEN escalate: when the decision can't tell how hard a request is, don't automate it.

The fallback action is escalate, so other reaches a person too.

Test real messages in the Playground, then Deploy v1.

The decision schema

{
  "stateSchema": { "kind": "text" },
  "questions": [
    {
      "key": "intent",
      "type": "choice",
      "instructions": "What is the customer asking for?",
      "options": [
        { "value": "order_status", "description": "where is my order, tracking, delivery date (answered by a database lookup)" },
        { "value": "billing_question", "description": "charges, invoices, refunds, payment methods" },
        { "value": "technical_issue", "description": "the product or app is not working as expected" },
        { "value": "complaint", "description": "dissatisfaction, wants to escalate or talk to a manager" }
      ]
    },
    {
      "key": "complexity",
      "type": "score",
      "instructions": "How complex is it to resolve this request?",
      "scale": ["simple", "moderate", "complex"]
    }
  ],
  "policies": [
    { "field": "intent", "on": "output", "operator": "eq", "value": "complaint", "action": "escalate" },
    {
      "field": "intent",
      "on": "output",
      "operator": "eq",
      "value": "technical_issue",
      "and": [{ "field": "complexity", "on": "score", "operator": "gte", "value": 2.5 }],
      "action": "escalate"
    },
    { "field": "complexity", "on": "confidence", "operator": "lt", "value": 0.5, "action": "escalate" }
  ],
  "settings": { "fallbackAction": "escalate" }
}

Call it

curl -X POST https://api.dcision.io/v1/decisions/customer-service-router \
  -H "Authorization: Bearer $DCISION_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "state": "My order #4821 was supposed to arrive yesterday. Can you tell me where it is?" }'
200 OK
{
  "decision_id": "dec_Lr5Tw9Mc2Qx7Hb4Nz1Kg",
  "execution_id": "exec_Fv1Wd6Kp3Zs8Qm2Tx7Lh",
  "schema": "customer-service-router",
  "version": 1,
  "result": { "intent": "order_status", "complexity": "simple" },
  "confidence": { "intent": 0.925, "complexity": 0.76 },
  "scores": { "complexity": 1.16 },
  "action": "continue",
  "action_reason": { "type": "default" },
  "metrics": {
    "latency_ms": 233,
    "engine": "jev",
    "model": "jev-1.13.0",
    "estimated_cost_usd": 0.00000966,
    "input_tokens": 230,
    "output_tokens": 21
  }
}

No rule matched: a simple order-status question that code can answer without any LLM.

Act on it

type Intent = "order_status" | "billing_question" | "technical_issue" | "complaint" | "other";

async function route(message: Message) {
  const response = await fetch("https://api.dcision.io/v1/decisions/customer-service-router", {
    method: "POST",
    headers: { Authorization: `Bearer ${process.env.DCISION_API_KEY}`, "Content-Type": "application/json" },
    body: JSON.stringify({ state: message.text }),
  });
  if (!response.ok) return humanQueue.add(message); // errors: a person decides

  const decision = await response.json();
  if (decision.action !== "continue") return humanQueue.add(message, { reason: decision.action_reason });

  switch (decision.result.intent as Intent) {
    case "order_status":
      return orders.replyWithTracking(message); // deterministic code, no LLM
    case "billing_question":
      return llm.answer(message, BILLING_SPECIALIST);
    case "technical_issue":
      return llm.answer(message, SUPPORT_SPECIALIST); // complex ones were escalated by rule 1
    default:
      return humanQueue.add(message);
  }
}

Tune it

  • Gate the intent itself. TypeSafe's version sends anything below 0.5 intent confidence to a person: set "minConfidence": 0.5 on intent to do the same.
  • Make the boundaries explicit. Describe each intent by what it covers and what it doesn't — option descriptions accept JSON rubrics such as { "what": "…", "not_for": "…", "examples": ["…"] }. See Questions.
  • Check the option order. Jev can lean toward the option listed first. Reorder the options in the Playground and check that the answers stay the same.
  • Describe other when "none of these" has a meaning for you — "not about an order, a bill or the app" — so off-topic messages reach the fallback action.
  • Grow the set as you learn. A choice holds up to 254 options plus other. Watch the other_option reasons in Executions to find the intents you are missing.
  • Route tools the same way. The Agent routing template applies this pattern to an AI agent's first tool.

Related: Confidence-gated routing · Support routing · Spam detection

On this page