← Blog

How to Build a Claude-Powered Ticket Triage Bot

2026-11-01 · 5 min read · SubToAPI Team

A ticket triage bot built on Claude reads incoming support tickets, classifies them by category and urgency, extracts key facts, and routes them to the right queue or person — without a human touching every single one first. You build it by sending ticket text to Claude with a structured prompt, forcing a predictable JSON output, and wiring that output into your helpdesk's routing rules.

The hard part isn't calling the model. It's getting consistent, machine-readable output every time, handling edge cases (spam, duplicates, VIP accounts), and keeping the whole pipeline cheap enough to run on every ticket you receive. Below is a practical build you can adapt to Zendesk, Freshdesk, Linear, or a homegrown queue.

What "triage" actually means for a bot

Before writing code, define what decisions the bot needs to make. Most triage bots need to output:

If you can't list these fields clearly, the prompt will be vague and the output will be inconsistent. Write the schema first, prompt second.

Architecture overview

A minimal pipeline looks like this:

  1. Ticket arrives via webhook (email, form, Slack, chat widget)
  2. Your backend sends the ticket text + metadata to Claude
  3. Claude returns structured JSON matching your schema
  4. Your backend applies business rules (e.g., "urgent" + "billing" → page on-call)
  5. Ticket gets tagged and routed in your helpdesk via its API

The only part Claude handles is step 2 and 3 — classification and extraction. Routing logic stays in your code, where it's testable and auditable.

Writing a prompt that returns structured output

Ask for JSON explicitly and give Claude a fixed set of allowed values. Vague prompts ("categorize this ticket") produce inconsistent labels over time; constrained prompts don't.

You are a support ticket triage assistant. Given a ticket, return ONLY valid JSON with this shape:

{
  "category": "billing" | "bug" | "feature_request" | "account_access" | "abuse" | "other",
  "priority": "low" | "medium" | "high" | "urgent",
  "sentiment": "neutral" | "frustrated" | "angry",
  "summary": string,
  "suggested_team": "support" | "engineering" | "billing_ops" | "trust_and_safety",
  "confidence": number between 0 and 1
}

Ticket subject: {{subject}}
Ticket body: {{body}}
Customer plan: {{plan}}

Keep the instruction set small. The more rules you cram into one prompt, the more likely Claude drifts on edge cases — better to add a second pass for anything genuinely ambiguous.

Calling the API

If you're already on an Anthropic subscription and don't want to juggle separate API billing, you can route this through SubToAPI, which turns your existing Claude access into a standard HTTPS API with application keys, usage metadata, and team seats. Setup is a signup plus one API call — see /docs/quickstart.

curl https://api.subtoapi.app/v1/messages \
  -H "Authorization: Bearer $SUBTOAPI_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "claude-sonnet-4-5",
    "max_tokens": 300,
    "messages": [
      {
        "role": "user",
        "content": "Triage this ticket and return JSON only.\n\nSubject: Refund not received\nBody: I cancelled last month but was charged again today. This is the third time.\nPlan: Pro"
      }
    ]
  }'

In JavaScript, wrap the call so the JSON parse failure path is explicit — this is the thing that actually breaks triage bots in production:

async function triageTicket(subject, body, plan) {
  const res = await fetch("https://api.subtoapi.app/v1/messages", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.SUBTOAPI_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      model: "claude-sonnet-4-5",
      max_tokens: 300,
      messages: [
        {
          role: "user",
          content: `Triage this ticket and return JSON only.\n\nSubject: ${subject}\nBody: ${body}\nPlan: ${plan}`,
        },
      ],
    }),
  });

  const data = await res.json();
  const text = data.content[0].text;

  try {
    return JSON.parse(text);
  } catch {
    return { category: "other", priority: "medium", confidence: 0, needs_human_review: true };
  }
}

Full request and response shape is documented at /docs/messages.

Letting Claude look things up before deciding

Simple text triage gets you far, but real triage often needs context the ticket text doesn't contain — customer plan tier, open ticket count, account age. Rather than stuffing all of that into every prompt, use tool calling so Claude only fetches what it needs for ambiguous cases:

Tool use lets the bot ask "is this customer on an SLA plan?" before deciding priority, instead of you pre-fetching data for every single ticket regardless of whether it's needed. See /docs/tools for the request format.

Handling the output

Once you have structured JSON, routing is plain code:

if (result.confidence < 0.6) {
  routeTo("human_review_queue");
} else if (result.priority === "urgent" && result.category === "billing") {
  routeTo("billing_ops", { page_oncall: true });
} else {
  routeTo(result.suggested_team);
}

Always keep a "send to human" fallback for low-confidence results. A triage bot that's wrong 5% of the time and silently misroutes tickets is worse than one that flags uncertainty and asks for a human to glance at it.

Testing and monitoring

Collect a set of real past tickets with known correct labels and run them through the bot whenever you change the prompt. Track:

For teams running this across multiple support agents or environments, usage breakdowns by key help separate staging traffic from production load — check /pricing for plan limits if you're scaling beyond a few thousand tickets a month.

Keep it narrow

Resist the urge to have the same call also draft a reply, summarize sentiment trends, and suggest KB articles. One prompt, one job — classify and route. Add a second, separate call for drafting responses once triage is stable. Bots that try to do everything in one shot are the ones that silently degrade when ticket volume or phrasing shifts.

Questions

Does the bot need to see the full ticket history, or just the latest message? For routing decisions, the latest message plus account metadata is usually enough. Full thread history helps for sentiment on repeat contacts, but it increases token cost — only fetch it via a tool call when the first pass is ambiguous.

What happens if Claude returns malformed JSON? Treat it as a parse failure, not a crash. Fall back to a default "needs human review" classification and log the raw response so you can see which prompt patterns cause drift.

Can this run in real time on every incoming ticket? Yes — classification calls are small (a few hundred tokens) and fast enough to run synchronously in a webhook handler. For very high volume, queue tickets and process them asynchronously so a slow response doesn't block your webhook receiver.

Turn your Claude access into an HTTPS API

SubToAPI gives you application API keys, streaming, tool use and usage insights on top of your existing Claude access — set up in minutes.

Start free  Read the quickstart →