Skip to main content
A support inbox fills overnight. Most of it is routine: a password reset, a question the help pages already answer. Some of it isn’t: a double charge, an angry customer, a question about pricing nobody has written down. The routine ones should be answered by the time the team sits down, and the rest should be waiting for a person with a draft already written. You want a model to sort the inbox and answer what it can, and you want a firm line: nothing about money, anger or a promise goes out without a person reading it. You want the line held by the run, not by the model’s own judgement of itself, so a classification the run can’t read is never treated as routine. Obversa makes the classification a typed answer and the routing a check you write. A Claude seat classifies each ticket and returns one JSON object; a check turns it into a route under a policy you set; the same seat drafts a reply for every ticket; then the run splits. A routine ticket the seat was sure about is answered through the helpdesk’s command. Everything else stops on a question for a person, with the draft attached. This file is one five-step run per ticket, and every ticket leaves a record of how it was routed and why.

Run it

Set the project up as Installation describes. Copy the file with briefs/, help/, tickets/ and policy.json beside it, sign in to Claude Code, and run it from that directory. Set HELPDESK_URL to your helpdesk’s endpoint, or leave the fictional default:
Terminal
The output below is the proof’s offline run, with scripted seats standing in for the models, so the words are the script’s and the shape is the run’s.
Output, from the offline proof
Four tickets, one answered, three for a person. The password reset was routine and the seat was sure, so its reply went to the helpdesk. The export question was routine too, but the seat’s confidence was under the policy, so it stopped for a person. The double charge needed a person. The seats question came back with a classification that wasn’t JSON, which the check refused to read as routine. All four have a draft in replies/, and history/ says which were answered and which wait, with the reason.

The file

The brief gives the seat its two jobs and the line it must not cross:
briefs/support.md
The routing is a function, not a model. It reads the typed classification, applies the policy’s confidence floor, and sends anything it can’t read to a person:
examples/use-cases/support/triage-with-escalation.ts (excerpt)
Each ticket is one dag() run. classify reads the ticket and help/ with read-only tools; route runs the function above; draft writes the reply whatever the route; then send and escalate each read the route and only one of them runs:
examples/use-cases/support/triage-with-escalation.ts (excerpt)
send is a command whose exit code is its result, so the helpdesk’s refusal would fail the step on the record. escalate is a person’s question with the ticket id and the draft’s path as its input; with nobody answering, the run stops there and the draft waits. Each ticket records to records/<ticket>.jsonl under its own run id.
examples/use-cases/support/triage-with-escalation.ts

The team’s shape

What the run did

The proof runs the file against a scripted seat and a stand-in for curl, and checks the routing the page describes: one call to the helpdesk and only for the routine ticket, three runs stopped on their question, four drafts on disk, and the unreadable classification filed with its reason. Obversa recorded each ticket’s run as its own event log, so the record for the seats question shows the classification the check couldn’t read, the route it chose, the draft, and the question, in order. The policy’s confidence floor is the author’s number, and the fixture exercises both sides of it. A high confidence clears the floor; it doesn’t prove the reply right, which is why the line about money, anger and promises is in the brief and the check, not in the seat’s judgement alone.

Next steps