Support triage
For: tenant administrators and plugin users.
ai.oz0/support-triage triages tickets in Jira Service Management on Atlassian Cloud or Data Center, or in ServiceNow. It reads and writes through the ticket system's connector and emails the requester through ai.oz0/email, using grants a tenant admin accepts at install.
The capability holds no credentials and reaches no hosts itself. Its flow, evals and approvals provide a reference for building one task on top of connector plugins.
| Kind | capability: one job, owned by the team that runs the service desk |
| Flow | support-triage: read, triage, ask a person when unsure, set the team, add a note, acknowledge |
| Agent | support-triage-agent: reads one ticket and answers its category, urgency, team, a reply and how sure it is |
| Tool | support_triage: the flow's own steps that need no model, and the hook on its replies; local, reaches nothing |
| Uses | ai.oz0/email, and optionally ai.oz0/atlassian, ai.oz0/atlassian-dc and ai.oz0/servicenow |
| Reaches | nothing of its own: every call to a ticket system or a mail server is its connector's |
"Verified" means the project tested, pinned and secured this plugin. It does not mean that Atlassian or ServiceNow endorses it.
What the flow does
- Which ticket. The flow's input names a system and a ticket (
{"system": "servicenow", "ticket": "INC0010001"}), or a routine fires it on a new ticket's event, whose subject names the ticket. For Jira without a ticket, it searches for the oldest untriaged one (SUPPORT_TRIAGE_JQL) and triages that. A search that finds none ends the job with "no ticket was read", as a completed job. - Read it through the connector the tenant has:
atlassian.getJiraIssue,atlassian_dc_jira.get_issue, orservicenow.get_incidentandservicenow.query_table(the caller's address, fromsys_user). On Jira Cloud the flow then labels the ticketoz0-triage(atlassian.editJiraIssue, keeping its other labels), so the next search leaves it out whatever happens to this job: see Limits. - Triage it.
support-triage-agentanswers a card:category(access, billing, hardware, software, network, how-to or other),urgency(high, medium, low),team(one ofSUPPORT_TRIAGE_TEAMS),reply,confidence(0 to 1) andreason. It has no tools: what the requester wrote is data it reads, never instructions, and it can change nothing. - Ask a person when unsure. When the confidence is under
SUPPORT_TRIAGE_MIN_CONFIDENCE(0.75 unless set), or the agent names a team the tenant does not list, the flow waits for a person to approve the triage, with the ticket, the team, the urgency and the reply in the request. A no ends the job, and nothing is written. The wait is 24 hours, and 30 minutes on Jira Cloud, whose search routine triages nothing else while a job of it waits. - Set the team: ServiceNow's
servicenow.update_incident(the assignment group and the urgency), Jira'satlassian_dc_jira.edit_issueoratlassian.editJiraIssue(the component). - Add an internal note with the triage and its reason: a ServiceNow work note (
servicenow.add_work_note), or a Jira Service Management internal comment on Data Center (atlassian_dc_jira.add_commentwith thesd.public.commentproperty). On Jira Cloud the flow adds none: see Limits. - Acknowledge the requester with
oz0_email.send: the agent's reply, to the address in the ticket.
Each write is the connector's function, behind its approval: the connectors' writes wait for an operator who did not start the job, and oz0_email.send waits for a person too. A ticket is outside input, so the job is tainted from its first read (and from its start when a routine fires it on a ticket's event), and the taint-default rule asks before each call that can send data out: every call through a connection counts, the reads included.
In such a job a person therefore also lets each read of the ticket through; a tenant that trusts its ticket system may narrow the rule's condition to leave those reads out (oz0 tenant tool-policy set --taint-default-when ..., with a reason). A call that the connector and the rule both ask for may wait twice. The flow's own steps (support_triage.start, ticket and route) are local and read-only, so they ask nothing.
The hook on replies
support_triage.hold_promises is a pre_tool_call hook on oz0_email.send in this flow. A
reply that promises money (an amount), a refund, a credit or a date ("by Friday", "within 2 days", a calendar date)
waits for a person, with the words that promise it in the request: only a person may promise that to a requester. The
hook may only let a call through or send it to a person (decisions: [allow, escalate]); it never changes what the
flow sends. A yes still leaves the send's own approval to decide.
Grants: the ticket systems are optional
The plugin declares what it calls of each connector by name under uses:, never <tool>.*. Email is needed; the
three ticket systems are optional: true:
| Plugin | Functions |
|---|---|
email | oz0_email.send |
atlassian (optional) | atlassian.searchJiraIssuesUsingJql, atlassian.getJiraIssue, atlassian.editJiraIssue |
atlassian-dc (optional) | atlassian_dc_jira.search_issues, atlassian_dc_jira.get_issue, atlassian_dc_jira.edit_issue, atlassian_dc_jira.add_comment |
servicenow (optional) | servicenow.get_incident, servicenow.query_table, servicenow.update_incident, servicenow.add_work_note |
So the install asks only for the functions of the systems the tenant has, and installs and grants none it lacks. The
flow's steps of a system it may not use are skipped (when: '"servicenow" in uses'), so a ticket of that system ends
the job with "no incident was read", never a failure.
- Installing a ticket system later says, in that install's output, what support-triage asks of it and the command
that grants it:
oz0 plugin grant support-triage --accept-uses <digest>. Until then its steps are skipped. - Revoking all of a system's functions (
oz0 plugin revoke support-triage --tool 'servicenow.*', or--tool 'atlassian_dc_jira.*') makes the flow skip that system again; its running jobs that needed it are stopped. Revoking only some of its functions does not: the system is still in reach, so the flow reads the ticket and fails at the step whose function was revoked. Revoke a system whole, or not at all. - Removing a ticket system the plugin uses optionally is not refused for it: the removal takes the system out of
support-triage's grant (audited
uses.revoke). A routine that fires on the system's events still holds the removal until you delete it.
Install
oz0 plugin install hub:ai.oz0/email --accept-reads mail
oz0 tenant mail-domains add default acme.com --reason "the service desk answers its own staff"
oz0 plugin install hub:ai.oz0/servicenow # or hub:ai.oz0/atlassian-dc, or hub:ai.oz0/atlassian
oz0 plugin install hub:ai.oz0/support-triage
The last install is refused once with the grants it asks and a digest; install again with --accept-uses <digest>.
Its evals run as the quality gate: the agent's cases on your tenant's models, the flow's on
recorded steps that call nothing, so they pass on a fresh tenant with whichever ticket system it has. Give the plugin
an owner team in the inventory, the team that runs triage.
Its agent asks for the tenant's default model alias with tools, never
a vendor's model.
Point it at your ticket system
Connect the ticket system's connector first, on its own page. Then save the routine that starts the flow; the
plugin's folder has one per system under routines/.
ServiceNow. Connect ai.oz0/servicenow (an OAuth app of your own in the instance) and its event source (a business rule that sends incident changes). Then:
oz0 routine save -f routines/servicenow-incidents.yaml # fires on each incident created
Jira Service Management, Data Center. Connect ai.oz0/atlassian-dc (the incoming application link's OAuth 2.0, or a personal access token) and its webhook. A JSM request is a Jira issue, so the flow reads it with Jira's functions; the team is the issue's component. Then:
oz0 routine save -f routines/jira-dc-issues.yaml # fires on each issue created
Jira Service Management, Cloud. Connect ai.oz0/atlassian's
atlassian-cloud connection: the Jira functions this plugin calls are Rovo's, on your own OAuth client. Do not assume
OAuth covers all of JSM on Rovo: its JSM Operations tools (alerts, on-call schedules) take only an API key, on the
separate atlassian-cloud-jsm connection, and this plugin uses none of them. Rovo sends no events, so a routine
searches every 2 minutes:
oz0 setting set SUPPORT_TRIAGE_JIRA_SITE https://acme.atlassian.net
oz0 routine save -f routines/jira-cloud-search.yaml # the oldest request SUPPORT_TRIAGE_JQL finds, one per fire
Each routine lists exactly the functions the flow calls, and its limits: 0.25 USD a fire and 10 USD a day unless you change them. To triage one ticket by hand:
oz0 run support-triage '{"system": "servicenow", "ticket": "INC0010001"}'
Settings
| Setting | What it is | Default |
|---|---|---|
SUPPORT_TRIAGE_TEAMS | The teams a ticket may go to, comma-separated, as the ticket system names them: ServiceNow assignment groups, Jira components | Service Desk,Access,Billing,Hardware,Network |
SUPPORT_TRIAGE_MIN_CONFIDENCE | How sure the triage must be to go ahead without a person, 0 to 1 | 0.75 |
SUPPORT_TRIAGE_JIRA_SITE | Jira Cloud's site, for the Rovo functions | none |
SUPPORT_TRIAGE_JQL | The search a Jira triage without a ticket runs, oldest first | statusCategory = "To Do" AND component is EMPTY ORDER BY created ASC |
What it reads and writes
| Reads | Writes | |
|---|---|---|
| The ticket system | the ticket's summary, description, reporter or caller, and the caller's address in ServiceNow | the team (assignment group or component), ServiceNow's urgency, one internal note |
| nothing | one message to the requester, with the tenant's AI label, only within the tenant's mail domains unless the tenant allows others | |
| Orchestrator Zero | the plugin's settings | the job, its steps and its approvals, as every flow's |
Tickets often hold personal data of the people who write them, colleagues and customers. The agent reads what the requester wrote; the job's steps keep it as any job's do, under the tenant's retention and access rules.
Limits
- Jira Cloud gets no internal note. Rovo's
addOrEditJiraIssueCommenthas no internal or public flag, so a comment would reach the requester; the flow sets the component and mails the requester, and adds no comment.
Jira Cloud is searched, one ticket a fire
Rovo sends no events, so its routine runs every 2 minutes and triages the first ticket of the search; a fire that finds none ends its job as completed. The routine runs one job at a time (overlap: skip), so while a job waits for a person, no other ticket is triaged: the triage's own approval waits 30 minutes at most there, but an approval the connector or the taint-default rule asks for waits as long as the tenant's approvals do. Decide them promptly, or narrow the taint-default rule.
A Jira Cloud ticket is triaged once
The flow labels each ticket oz0-triage right after reading it, and the search always leaves labelled tickets out, the tenant's own SUPPORT_TRIAGE_JQL included. A ticket whose triage a person refused, that waited past its timeout, or whose component could not be set keeps the label and no component: find them with labels = oz0-triage AND component is EMPTY and triage them by hand, or remove the label to have the routine pick one again. Only when the label's own write is refused does the next fire pick the same ticket again; label it, or narrow the search, to stop that.
- The team is a field the ticket system has. ServiceNow's assignment group and Jira's component must exist under
the names in
SUPPORT_TRIAGE_TEAMS; the plugin creates neither, and assigns no person. - The requester's address comes from the ticket. In ServiceNow it is the
sys_userrow whose name is the caller's, so two users with one name give the first one's address; on Jira, an address the site hides is not mailed. An address outside the tenant's mail domains is refused by ai.oz0/email. - The hook reads words, not intent. It holds replies with amounts, refund words and dates in English; a promise in other words or another language passes it, and the send's approval is then the check.
- A no to the triage ends the job. The person who decides cannot change the team in the approval; send the ticket by hand in the ticket system, or change the setting and run the flow again.
- Tested against fakes. The flow, the grants and the approvals are tested against fakes of Jira Data Center,
ServiceNow and an SMTP relay, and the flow on recorded steps for Jira Cloud. Runs against a live Jira Service
Management or ServiceNow have not been part of these tests; JSM Data Center's internal comment
(
sd.public.comment) is from Atlassian's documentation, not a live instance.
GitLab
The verified connector ai.oz0/gitlab - GitLab's issues, merge requests, pipelines and repositories through GitLab's own MCP server, on GitLab.com or your own instance, on your own OAuth application, with GitLab's webhooks as events.
Essentials
The verified set ai.oz0/essentials - the verified ai.oz0 plugins, at versions chosen to run together, installed as one decision.