This agent watches Zendesk and, for every ticket, drafts a full triage — what it's about, what to do, and the evidence behind it — without ever changing the ticket. It runs in observe mode: everything it produces is a draft on this dashboard for a human to use. Here's exactly what happens, when, and how — for both new and updated tickets — plus how the system teaches itself to get better over time.
The pathFrom a Zendesk event to a triage on your screen
Every ticket takes the same eight-stage journey. Stages 1–4 decide whether and when to run; stages 5–8 are the actual triage.
A Zendesk trigger fires
Zendesk's “GHE Agentic Workflow” trigger fires when a ticket is created or updated and POSTs a small signed webhook to the container. It carries just the ticket id — the container always re-fetches the live ticket, so it never works from stale data.
POST /webhook/zendeskDebounce & coalesce
A brand-new ticket is triaged immediately so it appears fast. Updates are debounced: the first edit schedules one re-triage ~10 minutes out, and any further edits in that window fold into that single run — so a flurry of edits becomes one re-triage, not one per edit.
Queue
Runs are processed one ticket at a time (a serial queue), so nothing collides. Within a single ticket, the sub-agents in stage 6 still run in parallel.
Loop guard
A safety check skips the run if the ticket's latest activity came from the bot itself — defense against triage loops. (In observe mode the agent barely touches Zendesk, so this rarely trips.)
Read the screenshots when there are any
A third of tickets carry an image, and on the ones that matter the text collapses to almost nothing. Ticket 181537 is a P1 with multiple locations down and a six-word description: “image attached of the error message.” Triage used to classify that blind. Now every screenshot is transcribed and described before the classifier runs, so the error text inside the picture becomes searchable text for every step below — the classifier, the knowledge search, the dedup match, all of it. No sub-agent had to change: they read the transcription out of the same ticket context they already open.
attachments[], so we read the message HTML too (33% of tickets, vs. 10% looking only at attachments)image001.png every time. A content hash can: a signature is byte-identical across hundreds of tickets, a screenshot is uniqueEnrich → Classify → Route → Sub-agents
The heart of the run: pull the full ticket + comments + metadata — including the owner (assignee), the group that owns the ticket, requester, support swim lane and billing status — classify it across seven dimensions, decide which specialist sub-agents are needed, and run them. Detailed below ↓
Synthesize the proposal
All the sub-agent output is merged into one coherent proposal: an internal note, an optional customer-reply draft written in the ticket owner's (assignee's) voice, proposed tags & Jira actions, and a single recommendation — resolve, suggest, escalate, or needs review.
Save → dashboard
Token usage & cost are tallied per step, the record is saved, and it shows on the triage dashboard within seconds. Re-triages append to the ticket's run log, so you can see the true cost per ticket over time.
ai-reviewed tag (once) + a link back to this analysisTimingNew tickets vs. updated tickets
The only difference is when the run happens. The triage itself (stages 5–8) is identical.
Incoming immediate
A ticket we've never seen triages right away.
- t = 0s — Ticket created in Zendesk → webhook fires
- t = 0s — First sighting → runs immediately
- ~30–90s — Classify + sub-agents + synthesize
- done — Appears on the dashboard;
ai-reviewedtag + triage link written once
Updated debounced
Edits tend to arrive in bursts, so we wait for the dust to settle.
- t = 0m — Ticket edited → schedules a re-triage for +10m
- t = 2m — More edits → fold into the same pending run
- t = 10m — One re-triage fires, re-fetching the latest ticket
- done — Dashboard refreshes; run log gains one entry (no re-tag)
Inside a runClassify, then fan out to specialists
Every ticket is first read by the classifier. Its answers decide which specialist sub-agents run — so a simple how-to and a production bug take very different paths.
🖼️ Image understanding Haiku 4.5 · only when the ticket has screenshots
Runs before the classifier and writes what it sees into the ticket context, so the error text in a screenshot is ordinary searchable text by the time anything below reads it. On a ticket whose whole description is “image attached of the error message,” this is the difference between triaging the problem and triaging six words.
🧠 Classifier Sonnet 5 · runs on every ticket
Reads the ticket, comments, Zendesk metadata and any image transcriptions from above, then decides seven things — each with a confidence score:
Knowledge resolve draft answer
Searches the KB + product docs and drafts a first-pass answer.
Fires for: how-to, configuration, printing, access, report issues — or as the fallback so every ticket gets a drafted proposal.
Dedup & link find related
Searches Jira + dev history for duplicate or related issues and proposes links.
Fires for: bugs, enhancements, data / performance / sync issues, or anything headed to dev.
Escalation package for dev
Assembles repro steps, affected module, and evidence for confirmed bugs.
Fires for: escalate to dev / integration / infrastructure.
Product signal state the need
States the need behind an enhancement request and drafts the PD Jira item for it — read-only, nothing filed.
Fires for: tickets the classifier calls an enhancement request, and only while the feature is switched on — see below.
Config assist setup steps
Looks up configuration / how-to steps from the knowledge base.
Fires for: configuration issues or “apply configuration” actions.
What ends up on the ticket
Evidence it attaches
Similar past tickets — each linked straight into Zendesk — plus the docs, KB articles and related dev work items it actually used, so you can check its reasoning rather than take it on trust.
Same-customer history
How many tickets this dealer has already filed about the same product or module recently, with clickable examples — and a ↻ badge when it judges the issue genuinely recurring for them.
Sliceable on the grid
Group, module, product, issue type, dealer, owner, resolution, urgency, billing status and support swim lane are all filters on the triage dashboard. Group comes first because it is a scope — pick your team and the page becomes that team's queue. It is a different field from the support swim lane, and the one that works for every team: swim lanes are support's own product-area tagger, and most other groups do not use them.
A second questionIs this ticket also product feedback?
Support's question is “how do we resolve this ticket.” Product's is “what does this ticket tell us to build.” The same triage run can answer both, because it has already read the ticket — so a sixth specialist states the need behind an enhancement request and drafts the PD Jira item a Product Owner would act on. Nothing is created in Jira. The draft stops on the dashboard, waiting for a human, and the step that would file it does not exist yet.
Why it is a separate agent and not another classifier question
Only ~9% of tickets need it
A deep extraction that most tickets do not need does not belong in the prompt every ticket pays for. The classifier already decides whether a ticket is an enhancement request; this agent only runs on the ones where it said yes, so the other 91% cost nothing.
The classifier is eval-gated
Its issue-type answer is one of only two dimensions graded against what humans actually did. Adding a field to that prompt can shift its other answers, so it would need a full eval run to land. A separate agent asks the same question at zero blast radius.
It cannot break a triage
It runs inside the same parallel block as the other specialists and under the same rule: if it fails, that one drawer section is missing and everything else about the ticket is unaffected. A ticket that is not an enhancement request is triaged byte-for-byte as before.
The need, not the request
This is the whole reason the agent exists rather than a keyword filter. A customer asks for the thing they can picture; the need is the outcome they are trying to reach. Getting it wrong forecloses the design before anyone has looked at it — so what they asked for is recorded separately, as one option among several, and never as the problem statement.
| What the ticket says | What the draft says |
|---|---|
| “Can you add a markup column to the PO screen?” | Parts managers cannot see transfer markup when they create a purchase order, so margin errors surface only at month-end close. |
| “We need a report.” | The dealership needs to reconcile vendor credits against received POs to catch margin leakage before the period closes. |
What the code checks, so the model is not taken on trust
The quote is real
A customer quote must appear verbatim in a message a customer actually wrote — checked against the ticket's own comments, with the author's role resolved from Zendesk. A paraphrase, or a line lifted from a Procede engineer's reply, is discarded and the draft flagged.
The item type is derived, not chosen
Whether the draft is an Epic or a Defect is decided in code from the evidence the ticket carried — documented behaviour or a regression makes it a Defect. The sections the body is then checked against cannot be picked by the same output being checked.
Some drafts cannot be filed at all
PD refuses a create without a Feature, and a few of our modules — add-ons especially — map to no honest Feature value. Those drafts are marked unfilable with the reason, rather than being offered to a reviewer as ready.
Who does the thinkingModels & cost
Reasoning-heavy steps use Sonnet; mechanical search/lookup steps use the cheaper Haiku. The learning loop (below) uses Opus, because a prompt edit shapes all future triage. List pricing, per million tokens:
| Model | Used for | Input | Output |
|---|---|---|---|
| Opus 5 | Learning loop — authoring prompt edits | $5 | $25 |
| Sonnet 5 | Classify · Knowledge · Escalation · nightly analysis | $3 | $15 |
| Haiku 4.5 | Image understanding · Dedup & link · Config assist | $1 | $5 |
Every run records its exact tokens and cost per step — see the Analytics page for spend by model, by module, and per day.
GuardrailsObserve mode — what we do & don't touch
✅ What it writes
Two harmless breadcrumbs, and nothing else: an ai-reviewed tag added once on a ticket's first triage, and a Procede AI Triage Link field holding a one-click link from the Zendesk ticket back to this analysis.
🚫 What it never does
No comments or replies to customers. No status, priority, assignee, group or swim-lane changes — it never re-routes a ticket to another team. Every reply, note, tag and Jira action it produces is a draft shown here, applied only if a human chooses to.
♻️ Why that can't loop
Writing the link field counts as updating the ticket, which would normally trigger another triage. It writes the link only when the field doesn't already hold it — so the run it triggers finds nothing to change and stops. The loop ends on the data, not on a flag that could fail open.
🙂 Your feedback
On any ticket you can 👍 or 👎 the proposed action and leave a note either way — a correction on a 👎, or praise-with-a-caveat on a 👍. That verdict and note are saved to the ticket and become the strongest signal in the learning loop.
Across every ticketCatching a pattern before someone reports it twice
Everything above concerns one ticket at a time. The Trending page reads them together and looks for shape — what's spiking, which dealers are struggling, and what is breaking out right now.
📈 Leaderboards & trend lines
Volume by product, dealer, module, issue type and owner, plus a time-series you can pivot by any of them. Click anything to land on that slice of the triage grid.
🔥 Hot / emergent
What is arriving faster than normal for that particular thing — ranked on how unusual the volume is, how severe the tickets are, and how many dealers are affected.
🚨 Burst alerts
Five or more tickets of the same kind within three hours from three or more different dealers raises a callout here and on the main dashboard.
↻ Dealers heating up
Dealers filing above their own normal rate — scored the same way, but ranked against other dealers so a small customer having a bad week isn't buried by a big one. They drop off once back to normal.
Getting smarterThe self-improvement loop
Once a week the system grades its own recent work against what humans actually did, proposes one small fix to its own prompts, proves the fix does not regress anything, and opens it for review. A human is always the gate — nothing ships unattended. It is a loop with exits, not a circle: most weeks it stops early, on purpose.
What runs, when, and what it costs
Nothing here is triggered by a person remembering to. Every row is a schedule, and every one of them fails loudly rather than going green empty.
| What | When | Cost per run |
|---|---|---|
| Triage a ticket — the pipeline above | on every Zendesk create or update, four at a time | ~3.6 min |
| Capture outcomes — re-read the ticket, record what humans did | daily, 05:00 UTC | no AI spend |
| Reconcile — ask Zendesk what we never stored | daily, 06:00 UTC | no AI spend |
| Grade the week and dispatch one proposal | Saturday, 03:00 UTC | the 14 gate fixtures, twice — ~5 min, ~$5 a pass |
| Verify whether last week’s merged change actually moved production | Saturday, 04:00 UTC | no AI spend |
| Watchdog — did the loop run, and did any of it matter | Monday, 10:00 UTC | no AI spend |
| Golden-set audit — is the eval still measuring the right thing | monthly, 1st at 07:00 UTC | no AI spend |
| Field-tier eval — does the set still look like production | monthly, 2nd at 08:00 UTC | 19 fixtures — ~7 min, ~$7 |
What the golden set is — and what it isn’t
The eval scores the classifier against tickets whose right answer a human has already written down. That set does two different jobs, and confusing them is how it quietly stopped being useful.
🔒 gate — 14 fixtures
The regression guard. Every proposal runs it, before and after. These are the unambiguous ones: a fixture that flaps here would block good changes at random, so nothing joins this tier until it has held still.
🎯 field — 19 fixtures
Not a guard — a mirror. It asks whether the set still resembles the tickets actually arriving. That is a monthly question, so it runs monthly rather than charging every proposal for it.
💸 Why it can’t just keep growing
Every fixture costs ~$0.37 and ~0.37 minutes per pass, forever. Two passes inside a 180-minute job puts a hard ceiling at 241 fixtures — and the money bites long before the clock does. “Add more fixtures” is usually aimed at the wrong constraint.
🚧 What it still can’t tell us
Which fixtures have never once changed a verdict — i.e. which are pure overhead. Every eval run is now recorded so that becomes answerable, but it needs a few months of history before “it never moved” says anything about the fixture rather than the sample size.
Loop memory — so it stops re-sending a fix you already declined
| State | What it means | Blocks a repeat for |
|---|---|---|
proposed | A PR was opened. Nobody has decided yet. | 14 days |
rejected | A human looked at it and said no. | 90 days |
merged | It shipped, and its effect has not been read yet. | its dimension, for one cycle |
rejected is a durable human verdict, so it binds for a long time. proposed is merely the absence of one — and it expires fast, because an entry that nothing ever flips reads forever as “a human is still reviewing this”. That is not hypothetical: one week the loop proposed nothing at all because two rows sat at proposed long after their PRs had closed, silently suppressing the two largest routing patterns with no way to recover. Expiry makes the suppression self-terminating by data instead of dependent on one event firing exactly once.Most of the machinery above exists because something was being measured wrongly. Each of these cost real weeks:
service/src/pipeline/models.ts; the debounce window in TRIAGE_UPDATE_COOLDOWN_MINUTES; the burst-alert and hot/emergent thresholds in TRENDING_ALERT_* and TRENDING_HOT_*. This page is documentation — see the dashboard for live triage.