Skip to content

Support Triage Agent

Classifies inbound support tickets by urgency and product area, writes the classification back to the ticket store, and pages a human for anything it marks P1.

Repository: github.com/velerion/example-support-triage (TODO(velerion): confirm the repository name before publishing.)

Terminal window
git clone https://github.com/velerion/example-support-triage
cd example-support-triage
npm install
cp .env.example .env.local # fill in DATABASE_URL and PAGERDUTY_TOKEN
velerion env use dev
velerion connection create tickets-rw --type postgres --env dev --secret DATABASE_URL
velerion mcp install velerion/postgres@2.4.1 --env dev --connection tickets-rw
npm run seed # 200 synthetic tickets
velerion dev --agent support-triage
  • A custom tool with a Zod-validated input schema and a real side effect.
  • Structured output enforced by a schema rather than parsed from prose.
  • A deliberate human-in-the-loop boundary: the agent may propose a P1 but a person confirms it.
  • An evaluation suite that runs in CI against a fixed ticket set.
import { defineAgent, mcp, tool, output } from '@velerion/sdk';
import { z } from 'zod';
const Triage = z.object({
urgency: z.enum(['P1', 'P2', 'P3']),
productArea: z.enum(['billing', 'auth', 'ingest', 'ui', 'other']),
// Forcing an evidence list makes low-quality classifications visible in
// review instead of hiding behind a confident-sounding summary.
evidence: z.array(z.string()).min(1).max(4),
needsHuman: z.boolean(),
});
export default defineAgent({
name: 'support-triage',
model: 'claude-opus-5',
output: output(Triage),
instructions: `
Classify the ticket. Quote the exact ticket fields that drove your decision
in "evidence". Set needsHuman when the ticket is ambiguous, mentions data
loss, or names a customer on an enterprise plan.
`,
tools: [
mcp('velerion/postgres', { connection: 'tickets-rw', tools: ['query'] }),
tool({
name: 'record_triage',
description: 'Persist the triage result against the ticket.',
input: z.object({ ticketId: z.string(), triage: Triage }),
async run({ ticketId, triage }, ctx) {
await db.tickets.update(ticketId, triage);
ctx.logger.info('recorded', { ticketId, urgency: triage.urgency });
return { ok: true };
},
}),
],
});
const run = await velerion.agents.run('support-triage', { input: { ticket } });
if (run.output.urgency === 'P1' || run.output.needsHuman) {
// The agent never pages directly. It raises a candidate; on-call confirms.
await queue.push('p1-review', { ticket, triage: run.output, runId: run.id });
} else {
await db.tickets.update(ticket.id, run.output);
}

Keeping the page behind a human confirmation is the whole point of the example. An agent with a direct paging tool will eventually wake somebody at 03:00 for a duplicate ticket.

Terminal window
npm run eval
support-triage eval/tickets.jsonl (200 cases)
urgency exact match 91.5% (target 85%)
product area exact match 88.0% (target 85%)
P1 recall 97.1% (target 95%)
P1 precision 74.3% (target 60%)
evidence non-empty 100.0% (target 100%)

P1 recall is weighted far above precision: a missed P1 is an outage, a false P1 is a minute of someone’s attention. Set your own targets deliberately rather than optimising a single accuracy number.