Skip to content

Support ticket triage ​

Triage of an inbound support ticket, decided by a model, rules and a support agent together in one durable workflow: your helpdesk sends the ticket with the customer, a model says whether it is spam, whether the help center answers it, what it is about and how urgent it is, five rules weigh those answers with the customer's tier and repeat tickets, and each ticket an agent must handle gets a summary the model wrote and a case at the priority its urgency gives. Every answer, rule result and decision stays on the record. Pack: examples/policies/support-ticket-triage/.

A pattern for a support triage decision, not customer-service advice: the thresholds, tiers, questions and SLAs are placeholders chosen to make the example run; a real policy comes from the business's own rules. Its model steps call an install named chatgpt (The chatgpt install).

The decision it automates ​

Whether a ticket needs an agent, and how soon, when your helpdesk starts the run. A ticket is not an application, so the outcomes mean:

  • approve: resolved without an agent. A how-to the help center answers, at routine urgency, from a customer on the free tier or a sender without an account: your helpdesk answers from its help center and closes the ticket, which the customer can reopen.
  • reject: closed as spam, without a reply. Only when the model is at least 90% sure and the sender has no account: a customer's ticket is never closed on the model's answer alone.
  • manual_review: an agent handles it, in a support_ticket case at the priority the urgency gives. The run waits for the agent, whose decision ends it with the same meanings: approve once the ticket is resolved, reject when it is closed without an answer (spam the rules let through, a duplicate).

Spam is a reject whoever closes it, the rules or an agent, so the outcome alone tells your helpdesk whether to reply. The pack writes no reply to the customer: on the approve path your helpdesk sends its own help center's answer.

Data expected ​

  • The customer as a subject (kind user) with tier (none for a sender without an account, free, pro or enterprise) and the ticket under ticket (subject, message, product, channel), which your helpdesk writes before it starts the run; the ticket's id as the run's correlationId finds the run again. Contact details such as email, phone and company may sit beside them; the model never reads them.
  • A ticket.created event for every ticket, this one included, posted through POST /events.

Steps and rules ​

StepTypeWhy
gatebranchA ticket with a message goes on to classify; one without goes to an agent (case_medium), without the model or the rules.
classifycall_appchatgpt's decide, its state subject.ticket: the four questions below, answered under triage.
rulesevaluate_rulesThe support-ticket-triage rule set under max_severity, the model's answers among the data it weighs.
routebranchrules.outcome == manual_review goes on to the summary and a case; an approve or a reject ends the run.
summarizecall_appchatgpt's generate: an object of the step's schema, brief.output.summary and brief.output.nextStep, for the agent.
prioritizebranchtriage.answers.urgency.value picks the case: 3 critical, 2 high, 1 medium, otherwise low.
case_critical, case_high, case_medium, case_lowcreate_casesupport_ticket at that priority, with SLAs of 1, 4, 24 and 72 hours; the topic is the case's routing label (queueFrom).
decideemit_decisionFrom the rules, or the agent's decision.

The rule set support-ticket-triage (useCase: custom) uses max_severity: a failed block rule rejects, a failed warn rule sends the run to review, and the weights add up to the score. Three of its rules read the model's answers, so classify runs before it. The one block rule needs the model's answer and the sender's tier together; the model's other answers only warn.

RuleTypeSeverity, weightFails when
ticket_spamexpressionblock, 100triage.answers.spam.probability is at least 0.9 and subject.tier is none; a missing tier is an error, which goes to review
ticket_paying_customercomparisonwarn, 30subject.tier is pro or enterprise
ticket_urgencyscore thresholdwarn, 30triage.answers.urgency.value is above 0
ticket_self_servecomparisonwarn, 20triage.answers.selfServe.value is not true: the model is less than 80% sure
ticket_repeatvelocitywarn, 20more than two ticket.created events in 24 hours

The model steps ​

classify asks four questions about one state, the ticket alone, which its input mapping takes as the decision's state (subject.ticket) (Asking for decisions):

QuestionAnswerAsksRead by
spamyes or noWhether the ticket is unsolicited marketing, a phishing attempt or asks nothing of supportticket_spam
selfServeyes or no, threshold 0.8Whether the help center answers it on its own: how to do something the product already doesticket_self_serve
topichow_to, account, billing, feature_request or bugWhat the ticket is aboutsummarize, and the case's routing label
urgencya score on four levelsFrom routine (0), through soon and today, to now (3): many people blocked, or money or data at riskticket_urgency, prioritize

summarize runs only on the way to a case. It asks generate for a TicketBrief from the ticket and the topic classify chose: what the customer needs, in two sentences at most (summary), and the one action the agent should take first (nextStep). Its instructions forbid promising a refund, a credit or a date (Asking for an answer). The brief is for the agent: nothing the model writes reaches the customer.

What the vendor sees ​

Through the install's connection, the steps' own texts and the ticket (subject, message, product, channel), with the topic for the summary: not the customer's name, contact details, company or tier (Data protection). Anyone can write a ticket, and a sender may write instructions into it. The model reads it as data, and an answer it was talked into changes only how that sender's own ticket is handled: closed as spam when they have no account, left to the help center when they are not a paying customer, or seen sooner or later by an agent. A paying customer's ticket always reaches one (Prompt injection).

The chatgpt install ​

A pack never carries an install: this one declares the install its steps call, "installs": { "chatgpt": "chatgpt" }, an install of the platform's ChatGPT (OpenAI API) app. The import reports chatgpt missing until the workspace has one (--publish publishes all the same), and a run that reaches classify without it fails with unknown_install. The admin console's import result installs it in one click, with a preset configuration that reaches OpenAI's Responses API with storing off, for decisions too, and a sample key (Installs at import and export).

OpenAI refuses the sample key. Until an administrator stores the workspace's own OpenAI key on the install's page (Connect › Apps, the install chatgpt, Configuration › Keys › openaiApiKey), every run of a ticket with a message fails at classify: it gets no decision and opens no case, and OpenAI's answer shows in the invocation record and under the run's app calls. The same holds whenever a call fails for good, summarize's included, through an outage that outlasts its retries, an exhausted quota or a refusal (Time, retries and failures). A ticket without a message never reaches the model, so its case opens all the same. Once the key is stored, replay a failed run from the step that failed (Operate › Runs, Replay from step: Admin console: Runs). Every model app takes the same input and answers in the same shape, so the steps can call another app's install instead, declared under its own name, or fall back on one; Jev decides only, so summarize needs one that also generates (Falling back to another install).

What a reviewer sees ​

A support_ticket case at the priority the urgency gave, labeled with the topic, with the ticket, the model's answers and probabilities, the summary and next step under brief, each rule's result, and the model's calls under Vendor checks. A case queue filtered on the label gives a team its tickets, billing to the billing team (Admin console: queues and filters); a breached SLA raises the priority one level and sends case.sla.breached (SLAs). The agent's decision ends the run: approve once the ticket is resolved, reject when it is closed without an answer.

How to adapt it ​

Read the tier and the account's health from your CRM instead of the subject: install the platform's http-api for its API, add a call_app step after gate that maps the customer's email into the request's variables, and point the tier rule at its answer (Adding a vendor step). Try a lower spam threshold as a shadow entry of the rule set first, recorded on every run but deciding nothing (Rules); label cases by tier instead of topic with queueFrom: "subject.tier"; and add a draftReply property to the summary's schema, for the agent to edit in your helpdesk before sending it, so a person stays between the model and the customer.

Run it ​

bash
pnpm policy:import examples/policies/support-ticket-triage --publish

pnpm policies:check runs two fixtures against a model stub (scripts/ai-stub.mjs), whose answers a cue word in the message picks:

  • expect.json: an enterprise customer's ticket about checkout failing for every customer, cued high. The stub puts spam at 0.95, but the spam rule closes only tickets from senders without an account, so it passes; the urgency is at the top level and the tier and urgency rules fail, so the model summarizes the ticket and a critical support_ticket case labeled bug opens with a one-hour SLA; the check approves it and the run completes with a manual approve.
  • expect-spam.json: an SEO pitch from a sender without an account, cued high. Spam at 0.95 and no account fail the spam rule, and the run completes with an automated reject, without a summary or a case.

Released under the Apache-2.0 License.