Jev AI Hub
Start Learning

Routing · Mixed

Route Customer Support Tickets with Jev

Use Jev Choice, Noul, and Score together to route support tickets, flag urgency, and keep assignment logic in code.

Published
Sep 20, 2026
Updated
Sep 20, 2026
Last verified
Sep 20, 2026

Quick answer

Assign a ticket to billing, technical, account, or other, and score urgency in the same Jev call.

Problem

A support inbox mixes refunds, outages, and login problems. A single LLM prompt that returns free text is slow to parse and easy to break. You need a closed set of queues plus a separate urgency signal before auto-assignment.

Why Jev fits this task

Department is an unordered closed set — a Choice. Same-day risk is a yes/no judgment — a Noul. Frustration is an ordered rubric — a Score. Official docs say these can share one state and run in parallel, so you pay for the ticket text once.

Input state

Send only the fields the questions name. Official docs warn that extra unrelated state costs accuracy.

{
  "ticket": {
    "subject": "Charged twice for order A-104",
    "message": "I was billed twice for order A-104 this morning. Please refund the duplicate today — I need the cash back before payroll."
  },
  "customer": {
    "plan": "pro",
    "tenure_months": 14
  }
}

Question

Which team owns this ticket, does it need same-day action, and how frustrated is the customer?

Question type: Mixed.

Jev schema

{
  "model": "jev-latest",
  "state": {
    "ticket": {
      "subject": "Charged twice for order A-104",
      "message": "I was billed twice for order A-104 this morning. Please refund the duplicate today — I need the cash back before payroll."
    },
    "customer": {
      "plan": "pro",
      "tenure_months": 14
    }
  },
  "questions": {
    "department": {
      "type": "choice",
      "instructions": "Which team should handle `ticket.subject` and `ticket.message`?",
      "criteria": {
        "billing": "Charges, invoices, refunds, subscriptions, payouts",
        "technical": "Bugs, outages, integrations, product behavior",
        "account": "Login, permissions, profile, email change",
        "other": "None of the above"
      }
    },
    "urgent": {
      "type": "noul",
      "instructions": "Does `ticket.message` ask for same-day action or mention money, outage, or blocked work?",
      "criteria": {
        "true": "A deadline today, lost money, or a hard stop is stated",
        "false": "No deadline and no consequence is named"
      }
    },
    "frustration": {
      "type": "score",
      "instructions": "How frustrated does the writer sound in `ticket.message`?",
      "criteria": [
        "Calm, stating facts only",
        "Frustrated but civil",
        "Angry, threats, or all-caps abuse"
      ]
    }
  }
}

Python example

from typesafe_sdk import Choice, Noul, Score, TypeSafeClient

state = {
    "ticket": {
        "subject": "Charged twice for order A-104",
        "message": "I was billed twice for order A-104 this morning. Please refund the duplicate today — I need the cash back before payroll.",
    },
    "customer": {
        "plan": "pro",
        "tenure_months": 14,
    },
}

with TypeSafeClient() as client:
    response = client.system_one(
        state=state,
        questions={
        "department": Choice(
            instructions="Which team should handle `ticket.subject` and `ticket.message`?",
            criteria={
                            "billing": "Charges, invoices, refunds, subscriptions, payouts",
                            "technical": "Bugs, outages, integrations, product behavior",
                            "account": "Login, permissions, profile, email change",
                            "other": "None of the above",
                        },
        ),
        "urgent": Noul(
            instructions="Does `ticket.message` ask for same-day action or mention money, outage, or blocked work?",
            criteria={
                            "true": "A deadline today, lost money, or a hard stop is stated",
                            "false": "No deadline and no consequence is named",
                        },
        ),
        "frustration": Score(
            instructions="How frustrated does the writer sound in `ticket.message`?",
            criteria=[
                            "Calm, stating facts only",
                            "Frustrated but civil",
                            "Angry, threats, or all-caps abuse",
                        ],
        ),
        },
    )

print(response.answers["department"].choice)
print(response.model)

TypeScript example

import { choice, noul, score, TypeSafeClient } from "@typesafe-ai/sdk";

const client = new TypeSafeClient();

const response = await client.systemOne({
  state: {
    "ticket": {
      "subject": "Charged twice for order A-104",
      "message": "I was billed twice for order A-104 this morning. Please refund the duplicate today — I need the cash back before payroll."
    },
    "customer": {
      "plan": "pro",
      "tenure_months": 14
    }
  },
  questions: {
    department: choice("Which team should handle `ticket.subject` and `ticket.message`?", {
      billing: "Charges, invoices, refunds, subscriptions, payouts",
      technical: "Bugs, outages, integrations, product behavior",
      account: "Login, permissions, profile, email change",
      other: "None of the above",
    }),
    urgent: {
      type: "noul",
      instructions: "Does `ticket.message` ask for same-day action or mention money, outage, or blocked work?",
      criteria: {"true":"A deadline today, lost money, or a hard stop is stated","false":"No deadline and no consequence is named"},
    },
    frustration: score("How frustrated does the writer sound in `ticket.message`?", [
      "Calm, stating facts only",
      "Frustrated but civil",
      "Angry, threats, or all-caps abuse"
    ]),
  },
});

console.log(response.answers.department.choice);
console.log(response.model);

JavaScript example

import { choice, noul, score, TypeSafeClient } from "@typesafe-ai/sdk";

const client = new TypeSafeClient();

const response = await client.systemOne({
  state: {
    "ticket": {
      "subject": "Charged twice for order A-104",
      "message": "I was billed twice for order A-104 this morning. Please refund the duplicate today — I need the cash back before payroll."
    },
    "customer": {
      "plan": "pro",
      "tenure_months": 14
    }
  },
  questions: {
    department: choice("Which team should handle `ticket.subject` and `ticket.message`?", {
      billing: "Charges, invoices, refunds, subscriptions, payouts",
      technical: "Bugs, outages, integrations, product behavior",
      account: "Login, permissions, profile, email change",
      other: "None of the above",
    }),
    urgent: {
      type: "noul",
      instructions: "Does `ticket.message` ask for same-day action or mention money, outage, or blocked work?",
      criteria: {"true":"A deadline today, lost money, or a hard stop is stated","false":"No deadline and no consequence is named"},
    },
    frustration: score("How frustrated does the writer sound in `ticket.message`?", [
      "Calm, stating facts only",
      "Frustrated but civil",
      "Angry, threats, or all-caps abuse"
    ]),
  },
});

console.log(response.answers.department.choice);
console.log(response.model);

cURL example

curl -s https://api.typesafe.ai/v1/systemone \
  -H "Authorization: Bearer $TYPESAFE_API_KEY" \
  -H "Content-Type: application/json" \
  -d @- <<'EOF'
{
  "model": "jev-latest",
  "state": {
    "ticket": {
      "subject": "Charged twice for order A-104",
      "message": "I was billed twice for order A-104 this morning. Please refund the duplicate today — I need the cash back before payroll."
    },
    "customer": {
      "plan": "pro",
      "tenure_months": 14
    }
  },
  "questions": {
    "department": {
      "type": "choice",
      "instructions": "Which team should handle `ticket.subject` and `ticket.message`?",
      "criteria": {
        "billing": "Charges, invoices, refunds, subscriptions, payouts",
        "technical": "Bugs, outages, integrations, product behavior",
        "account": "Login, permissions, profile, email change",
        "other": "None of the above"
      }
    },
    "urgent": {
      "type": "noul",
      "instructions": "Does `ticket.message` ask for same-day action or mention money, outage, or blocked work?",
      "criteria": {
        "true": "A deadline today, lost money, or a hard stop is stated",
        "false": "No deadline and no consequence is named"
      }
    },
    "frustration": {
      "type": "score",
      "instructions": "How frustrated does the writer sound in `ticket.message`?",
      "criteria": [
        "Calm, stating facts only",
        "Frustrated but civil",
        "Angry, threats, or all-caps abuse"
      ]
    }
  }
}
EOF

Expected output

{
  "model": "jev-1.13.0",
  "answers": {
    "department": {
      "type": "choice",
      "choice": "billing",
      "probabilities": {
        "billing": 0.91,
        "technical": 0.05,
        "account": 0.02,
        "other": 0.02
      },
      "confidence": 0.88
    },
    "urgent": {
      "type": "noul",
      "noul": 0.94
    },
    "frustration": {
      "type": "score",
      "score": 1.1,
      "legend": {
        "0": "Calm, stating facts only",
        "1": "Frustrated but civil",
        "2": "Angry, threats, or all-caps abuse"
      },
      "probabilities": {
        "0": 0.05,
        "1": 0.8,
        "2": 0.15
      },
      "confidence": 0.7
    }
  },
  "usage": {
    "input_tokens": 340,
    "output_tokens": 42
  }
}

Confidence handling

If department.confidence is below your review floor, skip auto-assign and send to triage. Official confidence docs show example floors of 0.5 for uncertainty and 0.9 before a high-stakes action — those are examples, not defaults. Urgent.noul has no separate confidence field; threshold the probability itself.

Production considerations

Log response.model and the raw probabilities. Pin a versioned model ID after you tune thresholds. Add an other option so unknown tickets are not forced into technical. Do not send the customer's entire history — official notes say unrelated state hurts accuracy.

Helpdesk routers, shared inboxes, and first-line Slack bots that assign a queue before a human opens the ticket.

When to use Jev

You already know the legal queues and you want milliseconds plus a typed label.

When not to use Jev

The next step is writing a customer email, diagnosing a unique outage, or deciding refund eligibility from ledger math.

Jev will not write the reply, check whether a charge actually duplicated, or compare timestamps. Official jaggedness notes: keep arithmetic and date windows in code.

Common mistakes

  • Omitting other, then watching every weird ticket land in technical.
  • Hiding routing, urgency, and tone in one Choice.
  • Treating frustration.score 1.0 as exact without reading probabilities.

FAQ

Why not one Choice that includes urgent_billing?

Urgency and department are independent. Official docs recommend atomic questions, then compose in code.

Can I add more queues later?

Yes. Choice allows up to 255 options. Re-test boundaries when you add a neighbor queue.

Sources

  1. API referenceTypeSafe · accessed 2026-09-20 · documentation
  2. Primitives (Questions)TypeSafe · accessed 2026-09-20 · documentation
  3. ConfidenceTypeSafe · accessed 2026-09-20 · documentation
  4. Intent routingTypeSafe · accessed 2026-09-20 · documentation

All Jev examples