Skip to content
Tutorials

Evaluate a ticket

This tutorial routes a support ticket with decide(). The result is a typed value the code can branch on.

decide() asks typed questions about the ticket and gives back typed answers. In this tutorial, a server route returns a queue, an urgency, and a refund flag. It takes about 10 minutes.

1. Install

shell
npm i @tanstack/ai @tanstack/ai-typesafe

2. Set your key

Get a key from TypeSafe. Set it in the server environment:

shell
TYPESAFE_API_KEY=your-typesafe-api-key

Keep the key on the server. Do not send it to the browser.

3. Ask the questions on the server

Create src/lib/evaluate-ticket.ts. It asks three questions about the ticket. POST is a plain Web handler: it takes a Request and returns a Response.

ts
import { boolean, choice, decide, score } from '@tanstack/ai'
import { typesafeDecider } from '@tanstack/ai-typesafe'

type Ticket = {
  subject: string
  body: string
}

const adapter = typesafeDecider('jev-latest')

export function evaluateTicket(ticket: Ticket) {
  return decide({
    adapter,
    state: ticket,
    questions: {
      queue: choice({
        instructions: 'Which team should handle this ticket?',
        options: {
          billing: 'Payments, invoices, refunds',
          tech: 'Bugs, outages, integrations',
          sales: 'Pricing, upgrades, new accounts',
        },
      }),
      urgency: score({
        instructions: 'How urgent is this ticket?',
        levels: ['low', 'medium', 'high'],
      }),
      refund: boolean({
        instructions: 'Is the customer asking for a refund?',
      }),
    },
  })
}

export type TicketResult = Awaited<ReturnType<typeof evaluateTicket>>

function readTicket(body: unknown): Ticket | null {
  if (typeof body !== 'object' || body === null || !('ticket' in body)) {
    return null
  }
  const ticket = body.ticket
  if (typeof ticket !== 'object' || ticket === null) return null
  if (!('subject' in ticket) || !('body' in ticket)) return null
  if (typeof ticket.subject !== 'string' || typeof ticket.body !== 'string') {
    return null
  }
  return { subject: ticket.subject, body: ticket.body }
}

export async function POST(request: Request) {
  const ticket = readTicket(await request.json())
  if (ticket === null) {
    return new Response('Send { ticket: { subject, body } }.', {
      status: 400,
    })
  }
  return Response.json(await evaluateTicket(ticket))
}

4. Mount the route

Create src/routes/api.evaluate.ts. Start maps this file name to the /api/evaluate path.

ts
import { createFileRoute } from '@tanstack/react-router'
import { POST } from '@/lib/evaluate-ticket'

export const Route = createFileRoute('/api/evaluate')({
  server: {
    handlers: {
      POST: ({ request }) => POST(request),
    },
  },
})

5. Branch on the answers

Call the route from the client. TicketResult gives the answers their types, so queue.value is 'billing' | 'tech' | 'sales'.

ts
import type { TicketResult } from '@/lib/evaluate-ticket'

async function postTicket(ticket: { subject: string; body: string }) {
  const res = await fetch('/api/evaluate', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ ticket }),
  })
  if (!res.ok) throw new Error(`Evaluate failed with ${res.status}`)
  const result: TicketResult = await res.json()
  return result
}

const result = await postTicket({
  subject: 'Charged twice for the same invoice',
  body: 'Please refund the extra payment.',
})

if (result.refund.value) {
  console.log('refund path', result.queue.value, result.urgency.value)
} else {
  console.log('route to', result.queue.value, result.urgency.value)
}

Send that ticket. You get a queue, an urgency, and a refund flag, and your code branches on them.

6. Try it

  1. Run the app.
  2. Paste a ticket.
  3. Pick a provider and click Submit.

The same app is on the Examples tab at /ai/latest/docs/framework/react/examples/evaluate.

evaluate

The full example is on GitHub: TanStack/ai examples/react/evaluate.

How it works

decide() sends the ticket as state and asks every question about it. Each helper gives a different answer shape:

  • choice: value is one option key. You also get probability and confidence.
  • score: value is the nearest level label. score holds the raw number, and you also get probability.
  • boolean: value is true when the probability of yes is 0.5 or more. You also get probability.

The result also has a meta field with the model and the usage.

Swap the provider

The decide() call stays the same. Only the adapter changes:

  • OpenRouter: openRouterDecider('~typesafe/jev-latest') with OPENROUTER_API_KEY.
  • Vercel AI Gateway: vercelGatewayDecider('typesafe-ai/jev') with AI_GATEWAY_API_KEY.
  • Cloudflare: cloudflareDecider('typesafe/jev') with a Worker binding, or with CLOUDFLARE_ACCOUNT_ID and CLOUDFLARE_API_TOKEN.

For abort, middleware, and the full result shape, open the Evaluate guide.