Skip to content
Docs / Ticket triage, day one

Ticket triage, from day one

A helpdesk with no history: route each ticket to billing, technical, account or sales, rate its priority, and flag customers who might leave. Here we start in day-one mode, with a general model answering from the first call, and let each question graduate. The other way to start with nothing is to send the JSON you'd send Jev and have your own model in minutes, as in the zero-data bank cookbook.

1. Set it up

Save this as ticket-triage.json:

json
{
  "name": "ticket-triage",
  "questions": {
    "team": { "type": "choice", "instructions": "Which team should handle this ticket?",
              "criteria": { "billing": "invoices, charges, plans and refunds",
                            "technical": "bugs, errors, outages and integrations",
                            "account": "login, access, users and settings",
                            "sales": "pricing questions, upgrades and new contracts" } },
    "priority":   { "type": "score", "instructions": "How urgent is it?", "criteria": ["low", "normal", "high", "critical"] },
    "churn_risk": { "type": "noul", "instructions": "Is this customer at risk of leaving?",
                    "criteria": { "true": "they mention cancelling, switching or being fed up", "false": "they don't" } }
  },
  "decision_question": "team",
  "rules": [{ "id": "no-sales-on-outage", "description": "A ticket about an outage never goes to sales",
              "when": [{ "field": "is_outage", "op": "is_true" }], "block": ["sales"] }],
  "state_schema": { "fields": [
    { "name": "subject", "type": "text" },
    { "name": "body", "type": "text" },
    { "name": "plan", "type": "category", "values": ["free", "pro", "enterprise"] },
    { "name": "seats", "type": "number" },
    { "name": "is_outage", "type": "number", "description": "yes/no" }
  ] },
  "graduation": { "min_outcomes": 3000, "auto": true }
}
bash
curl https://api.canonopylabs.com/v1/domains -H "Authorization: Bearer $CANONOPY_API_KEY" \
  -H "Content-Type: application/json" -d @ticket-triage.json

2. Pick a day-one backend

A new domain starts with local, our built-in general model: no key, no cost. To use your own model instead, point it at any OpenAI-compatible endpoint:

bash
curl -X PUT https://api.canonopylabs.com/v1/domains/ticket-triage/fallback -H "Authorization: Bearer $CANONOPY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "provider": "openai", "base_url": "https://your-gateway.example/v1", "model": "your-model", "api_key": "<your key>" }'

Or Jev: { "provider": "typesafe", "api_key": "<your TypeSafe key>", "model": "jev-latest" }. See Day one.

3. Send tickets

bash
curl https://api.canonopylabs.com/v1/decide -H "Authorization: Bearer $CANONOPY_API_KEY" \
  -H "Content-Type: application/json" -d '{
    "model": "ticket-triage@latest",
    "state": { "subject": "API down", "body": "Our integration returns error 500 since this morning",
               "plan": "pro", "seats": 40, "is_outage": true }
  }'

Every answer comes back marked "source": "fallback", with your rule applied to the team decision:

json
{ "id": "dec_4c468a346f447e87", "model": "ticket-triage@day-one",
  "answers": {
    "team":       { "type": "choice", "choice": "technical", "confidence": 0.93, "source": "fallback", "route": "act", "...": "..." },
    "priority":   { "type": "score", "score": 2.1, "confidence": 0.85, "source": "fallback", "route": "act", "...": "..." },
    "churn_risk": { "type": "noul", "noul": 0.1, "source": "fallback", "route": "act" } },
  "decision": { "question": "team", "action": "technical", "confidence": 0.93,
                "blocked_by_rules": ["no-sales-on-outage"], "blocked_actions": ["sales"], "route": "act" } }

A day-one answer below 0.8 confidence comes back "route": "review" and waits in the unsure queue.

4. Report what happened

When an agent closes a ticket, send the real team, using the id from the answer:

bash
curl https://api.canonopylabs.com/v1/outcomes -H "Authorization: Bearer $CANONOPY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "decision_id": "dec_4c468a346f447e87", "action": "technical", "answers": { "priority": 2, "churn_risk": false } }'

Or upload last quarter's export in one go: a CSV whose columns are subject, body, plan, seats and team (add priority and churn_risk columns if you have them).

bash
curl https://api.canonopylabs.com/v1/domains/ticket-triage/data -H "Authorization: Bearer $CANONOPY_API_KEY" \
  -F file=@last_quarter.csv

5. Watch it graduate

bash
curl https://api.canonopylabs.com/v1/domains/ticket-triage/graduation -H "Authorization: Bearer $CANONOPY_API_KEY"
json
{ "min_outcomes": 3000, "auto": true, "questions": {
    "team":       { "status": "trained",  "version": 1,    "outcomes": 3202, "needed": 0 },
    "priority":   { "status": "fallback", "version": null, "outcomes": 1,    "needed": 2999 },
    "churn_risk": { "status": "fallback", "version": null, "outcomes": 1,    "needed": 2999 } } }

At 3,000 outcomes a question trains and switches to your own model on its own, question by question: its answers change from fallback to trained. Graduation is checked each time an outcome or an unsure-queue answer comes in. After an upload that takes a question past 3,000, it graduates with the next outcome, or train it straight away:

bash
curl -X POST https://api.canonopylabs.com/v1/domains/ticket-triage/train -H "Authorization: Bearer $CANONOPY_API_KEY"

Once at least 20 tickets that your backend answered have a reported outcome, the training report compares the two on those tickets: day_one is your backend's accuracy there. If your backend is still more accurate on them, the question keeps using it for now.