# Ticket triage

> Declare what a support ticket means for your business once, then apply it to every ticket. One request per ticket, however many fields.

Section: Scenarios · HTML: https://jevascript.org/docs/scenarios/ticket-triage · Markdown: https://jevascript.org/docs/scenarios/ticket-triage.md

**Source:** [`examples/launch/01-ticket-triage.ts`](https://github.com/hakantapanyigit/jevascript/blob/main/examples/launch/01-ticket-triage.ts)

## The problem

Inbound tickets need a priority, a queue and sometimes a page to on-call. The rules that decide this are half judgement (is this urgent? is the customer about to leave?) and half fact (what plan are they on? what is the SLA?). Mixing the two in one prompt makes both untestable.

## The schema

The frame says what the data is and what your words mean. The context carries policy the revenue team can edit. Every field is a proposition.

```ts
// triage.ts
import { boolean, defineSchema, enumOf, object, score } from "jevascript"

export const triage = defineSchema({
  name: "ticket-triage",
  version: "1",

  instructions: `
    Triage an inbound support ticket for Northwind, a B2B payments API.
    Customers are engineering teams integrating our SDK.
    "Blocked" means they cannot process live transactions right now.
    Sandbox and documentation problems are never blocking.
  `,

  context: {
    plans: {
      free: "Community support only. No SLA.",
      growth: "Business-hours support. 8-hour first response.",
      enterprise: "24/7 support. 30-minute first response. Named CSM.",
    },
    escalationPolicy:
      "Page on-call only for live payment failures affecting a paying customer in production.",
  },

  output: object({
    urgency: score(0, 100, "A human needs to act on this ticket urgently."),
    blocksRevenue: boolean({
      describe: "The customer is currently unable to take money from their own customers.",
      trueWhen: "Live transactions are failing, being declined, or not settling.",
      falseWhen: "Sandbox failures, slow dashboards, or questions about future work.",
    }),
    department: enumOf(
      {
        integration: "SDK usage, API errors, webhooks, authentication during integration",
        payments: "Declines, settlement, payouts, chargebacks, currency",
        billing: "Our own invoices, pricing, plan changes and refunds to the customer",
        security: "Credential exposure, suspicious access, vulnerability reports",
      },
      "Which team should own this ticket",
    ),
    sentiment: score(0, 100, "The person writing this sounds frustrated or angry."),
    churnSignal: boolean({
      describe: "The customer hints they may leave, or is evaluating competitors.",
      falseWhen: "Frustration alone. Anger is not the same as leaving.",
    }),
    needsHuman: boolean({
      describe: "A canned or templated answer would be inadequate here.",
    }),
  }),
})
```

## The decision

Six values come back from one request. Everything after that is TypeScript. Thresholds differ by what being wrong would cost, and they live where they can be reviewed.

```ts
// intake.ts
const t = await triage({ plan, subject, body })

const sla = plan === "enterprise" ? 30 : plan === "growth" ? 480 : null
const actions: string[] = []

if (t.blocksRevenue && plan !== "free") actions.push("page on-call")
if (t.urgency > 80) actions.push("pin to top of queue")
if (t.churnSignal) actions.push("notify CSM")
if (!t.needsHuman) actions.push("try autoresponder first")
```

Note `plan !== "free"`: the model can say a ticket blocks revenue, and the code still refuses to page for a free plan. The plan is a fact the customer cannot write into the ticket. See [Not a security boundary](https://jevascript.org/docs/not-a-security-boundary).

## In a service

The library repository contains a small application built around this schema: a `TicketService` with a database, a job queue and a keyword fallback for when the provider is down. Two details from it are worth copying.

**Cache the judgement.** The same ticket re-submitted (a retry, a webhook replay) must not drift across a threshold and produce a different priority:

```ts
const t = await triage(ticket, { cache: "30m", timeoutMs: 3_000 })
```

**Record the version.** Store which definition graded the ticket next to the result, so a change in wording never silently changes the meaning of old records:

```ts
gradedBy: `${triage.name}@${triage.version}`   // "ticket-triage@1"
```

## What to test

With the [mock provider](https://jevascript.org/docs/testing), each test states what the model would say and asserts what the code did:

- A blocking enterprise ticket → `critical`, `page-oncall` enqueued, and `provider.requestCount === 1`.
- The same answers on a free plan → no page, whatever the model said.
- A calm ticket with a churn signal → `notify-csm` and nothing else.
- Provider throws → the keyword fallback runs and `gradedBy` says so.
