Skip to content
Jev Guide

Start building / Guide 01 · TypeScript · Beginner

Route a support message in TypeScript.

A practical walkthrough for routing a support message to a department with TypeScript: department criteria, a typed answer, and the error handling a production caller needs.

Before you start

This guide assumes you already have a TypeSafe API key and @typesafe-ai/sdk installed. If you don't yet, the getting-started guide covers API access, billing basics, installing the SDK, and configuring TYPESAFE_API_KEY as a server-side environment variable, never in browser code.

The complete example

This is the same file used for the homepage excerpt and the message-routing recipe's own code panel, so it cannot drift from what those pages show.

message-routing.tsTypeScript
// Recipe: Route a message. Classify a customer message into the department that should handle it.
// Requires Node.js 20+ and TYPESAFE_API_KEY in the environment.
//   npm install @typesafe-ai/sdk
// Source: https://jevguide.ai/recipes/message-routing. Not executed against the live API by Jev Guide.
import { choice, TypeSafeClient } from "@typesafe-ai/sdk";

const client = new TypeSafeClient(); // reads TYPESAFE_API_KEY, defaults to jev-latest

const message = "Hi, I was charged twice for my subscription this month. The two charges show up on my card statement on the 3rd and the 4th. Can you refund one of them?";

const { answers } = await client.systemOne({
  state: message,
  model: "jev-latest",
  questions: {
    department: choice(
      "Which department should handle this customer message?",
      {
        billing: "Charges, invoices, refunds, payment methods, subscription costs",
        account: "Login, password, profile, permissions, closing or changing an account",
        technical: "Bugs, errors, outages, integrations, something not working",
        sales: "Pricing questions, plan comparisons, upgrades, purchasing for a team",
        other: "Anything that does not fit the departments above",
      },
    ),
  },
});

const { choice: result, probabilities, confidence } = answers.department;

// Your code decides what to do with the answer.
if (confidence < 0.5) {
  console.log("Unsure. Route to a person.", probabilities);
} else {
  console.log("Route to " + result + " (confidence " + confidence.toFixed(2) + ")");
}
Type-checked against @typesafe-ai/sdk 0.6.0 (pinned in package.json) · not executed against the live API by this guide

Inputs and outputs

Request fields

state
The customer message, sent as a plain string.
questions.department
A Choice question. Each option key carries a one-line description that reaches the model.

Response fields

choice
The option with the highest probability.
probabilities
One value per option; the API documents that they sum to 1.
confidence
A 0 to 1 statistic TypeSafe derives from how concentrated or spread out the probabilities are. It is not a guarantee that the choice is correct.

The five departments and their descriptions above are exactly what the homepage preview and the message-routing recipe use. Both descriptions and keys reach the model, so write them to distinguish the options from each other:

billing
Charges, invoices, refunds, payment methods, subscription costs
account
Login, password, profile, permissions, closing or changing an account
technical
Bugs, errors, outages, integrations, something not working
sales
Pricing questions, plan comparisons, upgrades, purchasing for a team
other
Anything that does not fit the departments above

Typed response handling

The SDK infers answer types from the question you build, so answers.department.choice is already typed as the union of the criteria keys you passed to choice(). There is no interface to hand-write or keep in sync. This file repeats the request so it compiles on its own.

handle-result.tsTypeScript
// Guide: typed response handling for the message-routing request.
// Requires Node.js 20+ and TYPESAFE_API_KEY in the environment.
//   npm install @typesafe-ai/sdk
// Source: https://jevguide.ai/guides/jev-typescript. Not executed against the live API by Jev Guide.
import { choice, TypeSafeClient } from "@typesafe-ai/sdk";

const client = new TypeSafeClient(); // reads TYPESAFE_API_KEY, defaults to jev-latest

const message = "Hi, I was charged twice for my subscription this month. Can you refund one of them?";

const { answers } = await client.systemOne({
  state: message,
  model: "jev-latest",
  questions: {
    department: choice("Which department should handle this customer message?", {
      billing: "Charges, invoices, refunds, payment methods, subscription costs",
      account: "Login, password, profile, permissions, closing or changing an account",
      technical: "Bugs, errors, outages, integrations, something not working",
      sales: "Pricing questions, plan comparisons, upgrades, purchasing for a team",
      other: "Anything that does not fit the departments above",
    }),
  },
});

const { choice: result, probabilities, confidence } = answers.department;

// TypeScript infers `result` as "billing" | "account" | "technical" | "sales" | "other"
// from the criteria keys passed to choice() above. No manual type annotation is needed.
function routeTo(department: typeof result): string {
  switch (department) {
    case "billing":
      return "billing-queue";
    case "account":
      return "account-queue";
    case "technical":
      return "technical-queue";
    case "sales":
      return "sales-queue";
    case "other":
      return "general-queue";
  }
}

// Missing a case above is a compile error here, not a silent runtime gap:
// the function must return a string on every path, and the switch is exhaustive
// only if every member of the union is handled.
console.log(routeTo(result), probabilities, confidence);
Type-checked against @typesafe-ai/sdk 0.6.0 (pinned in package.json) · not executed against the live API by this guide

Handling the result

The example branches on confidence: below 0.5 it treats the answer as unresolved and routes to a person; otherwise it acts on the chosen department. The 0.5 cutoff here is illustrative, not a value TypeSafe recommends universally. The official docs say a confidence threshold “is not one number” and should scale with the stakes of the action it gates, tuned against your own data. A destructive action deserves a higher bar than a read-only one.

Error handling

The HTTP API returns a JSON body describing what went wrong, and @typesafe-ai/sdk throws a typed subclass of TypeSafeError for each status below. Catch it and branch with instanceof. The official SDKs retry 429 and 529 with backoff by default, so most callers need no extra handling for those two. The example below still shows RateLimitError for the case where retries are exhausted.

401 Unauthorized
Missing or invalid API key. Check the Authorization header.
422 Unprocessable Entity
The request body failed validation, for example a missing field or malformed question.
429 Too Many Requests
Rate limit exceeded. Back off and retry after a short delay.
529 Overloaded
TypeSafe is temporarily overloaded. Retry after a short delay.
error-handling.tsTypeScript
// Guide: error handling for the message-routing request.
// Requires Node.js 20+ and TYPESAFE_API_KEY in the environment.
//   npm install @typesafe-ai/sdk
// Source: https://jevguide.ai/guides/jev-typescript. Not executed against the live API by Jev Guide.
import { AuthenticationError, choice, RateLimitError, TypeSafeClient, TypeSafeError } from "@typesafe-ai/sdk";

const DEPARTMENT_CRITERIA = {
  billing: "Charges, invoices, refunds, payment methods, subscription costs",
  account: "Login, password, profile, permissions, closing or changing an account",
  technical: "Bugs, errors, outages, integrations, something not working",
  sales: "Pricing questions, plan comparisons, upgrades, purchasing for a team",
  other: "Anything that does not fit the departments above",
};

const client = new TypeSafeClient();
const message = "Hi, I was charged twice for my subscription this month. Can you refund one of them?";

try {
  const { answers } = await client.systemOne({
    state: message,
    model: "jev-latest",
    questions: {
      department: choice("Which department should handle this customer message?", DEPARTMENT_CRITERIA),
    },
  });
  console.log(answers.department.choice);
} catch (err) {
  if (err instanceof AuthenticationError) {
    console.error("Invalid or missing TYPESAFE_API_KEY.");
  } else if (err instanceof RateLimitError) {
    // The SDK has already retried with backoff by the time this is thrown.
    const wait = err.retryAfterMs === undefined ? "after a short delay" : `in ${err.retryAfterMs} ms`;
    console.error(`Rate limited. Retry ${wait}.`);
  } else if (err instanceof TypeSafeError) {
    console.error("Jev request failed:", err.message);
  } else {
    throw err;
  }
}
Type-checked against @typesafe-ai/sdk 0.6.0 (pinned in package.json) · not executed against the live API by this guide

Limitations worth knowing

  • Jev currently accepts text input only: no images, audio, or video.
  • Confidence is a statistic TypeSafe computes from the shape of the probability distribution, not a guarantee that the chosen answer is correct. A flatter distribution means lower confidence. TypeSafe shows an approximation for illustration but does not publish the exact formula.
  • TypeSafe describes jev-1.13 as “extremely consistent”, meaning similar outputs for semantically similar inputs. It does not promise identical output for identical input, so treat any single answer as a judgment, not a fixed fact.
  • Rate limits and model aliases (like jev-latest) can change over time; pin a versioned model id if you need stable behavior across releases.

Still deciding whether this is the right kind of call for Jev versus a general-purpose LLM? See Jev vs LLMs: when to use TypeSafe AI. Building in Python instead? See the Jev Python tutorial, which also adds Score and Noul to the same kind of request.

Sources and verification

Testing status: all three files on this page are compiled by this site's TypeScript build against SDK 0.6.0, and their request shape was checked against the API reference in September 2026. Passing a type check confirms the code compiles against that SDK version. It is not the same as running it against the live API, which this guide has not done.