← All workflow playbooks

A Support Ticket Radar with Claude Code, Step by Step

Date: July 6, 2026 • Author: Second Axis

Something broke in production at 11pm, or Thursday's release moved a button and confused half your users, and the first system in your company that knows is the support queue. The tickets are already there: fifteen people describing the same CSV export failure in fifteen different ways. But your support team is answering tickets one at a time, which is their job, and nobody's job is to notice that today contains a cluster that didn't exist yesterday. So you find out in the Monday metrics review, or worse, on social media.

What you want is a radar: every morning, cluster the last 24 hours of tickets, compare against an established baseline, and stay quiet unless something is genuinely new or a known problem is spiking. Quiet is the important part: a digest that lists every ticket theme gets muted within a week; an alert that only fires above threshold earns its interruption and gets read every time.

Can you build this with Claude Code? Yes, and this post is the complete build. It is a daily-cadence variant of the conversation intelligence playbook, which is the anchor for every text-at-scale workflow in this series. The three-layer machinery (a cheap-model extraction pass, embeddings and clustering with stable cluster IDs, agents that read only cluster summaries) is explained in depth there and this post will not repeat it. What is covered here is everything the daily alerting mission changes: the ticket sources, the extraction schema, the baseline diff, the threshold math, and the delivery path that ends in Slack and Linear.


What you're building

Every morning before standup, without you touching anything:

  • Yesterday's tickets assigned to stable clusters ("CSV export fails", "billing page 500", "how do I invite teammates"), with volume and trend per cluster.
  • A loud alert when a new cluster crosses threshold: a group of tickets that fits no existing cluster and is too big to be noise. This is the "something just broke or the release confused people" signal, in Slack the next morning.
  • A spike alert on known clusters: a familiar problem running well above its baseline gets flagged too, with the baseline math shown.
  • Silence otherwise. No new cluster, no spike: nothing beyond a one-line "all quiet" heartbeat. The radar's credibility is its restraint.
  • Confirmed new clusters routed to Linear as tickets with volume, exemplars, and a verbatim user quote attached.

The scale math (and why it's different from the anchor)

Assumptions, stated so you can swap in your own: a 10K-user product generating 200-600 support tickets a day, each averaging 500-1,000 tokens once you include the customer's messages and any back-and-forth. That's roughly 100K-600K tokens a day, or 1-4 million tokens a week.

On a quiet day that almost fits in a 200K context window, so why not just paste the day's tickets into a prompt? Two reasons, and neither is raw size. First, the diff is the product. "Here are today's themes" is useless without "and this one did not exist yesterday," which requires cluster identity that survives from run to run; an ad-hoc reading every morning produces clusters that don't line up day over day, and the "new" detector dies. Second, cost: paying a frontier model to re-read every ticket daily is the expensive version of a job a cheap extraction pass does once per ticket, forever.

So the architecture is the anchor post's three layers, run daily instead of weekly: extract once per ticket with a small model via the Batch API, embed and assign to persistent clusters, and let the analysis agents see only cluster summaries. The daily cadence tightens every screw, and that's what the rest of this post is about.


Phase 0: Prerequisites

  • Claude Code installed and authenticated, on a Pro, Max, Team, or Enterprise plan for the scheduling and publishing steps.
  • An Anthropic API key with billing set up, separate from your subscription, for the extraction pass. Same reasoning as the anchor post: extraction is metered API spend, not plan usage; ticket volume keeps it small (numbers in the cost section).
  • Admin access to your helpdesk. Specifics in Phase 2; Intercom and Zendesk are not symmetric here.
  • Python 3.11+ with duckdb, pandas, scikit-learn (HDBSCAN), and an embeddings option (local sentence-transformers is fine at this volume).
  • A dedicated git repo. The recurring design depends on it: cluster state and baselines are committed, raw tickets never are.
  • A privacy decision, made up front. Tickets contain names, emails, and sometimes payment details, and the extraction pass sends ticket text to the API. That needs the same sign-off as any vendor; strip obvious PII in the export script if policy requires it.
  • Optional scaffolding: Anthropic's knowledge-work-plugins include a Customer Support plugin. Useful, but it does not contain the clustering pipeline, the baseline state, or the alert logic. That part is this playbook.

Phase 1: Repo and state layout

ticket-radar/
├── CLAUDE.md                    # rules: never read raw tickets into context
├── .mcp.json                    # helpdesk + Linear MCP (drill-downs only)
├── .claude/
│   ├── agents/
│   │   ├── cluster-analyst.md   # digs into one alerting cluster
│   │   ├── synthesizer.md       # writes the daily brief
│   │   └── critic.md            # judge loop before anything is sent
│   ├── skills/
│   │   └── ticket-radar/
│   │       └── SKILL.md         # the daily orchestration recipe
│   └── settings.json            # Stop hook → Slack run-status ping
├── scripts/
│   ├── export_tickets.py        # REST pull since watermark → data/
│   ├── extract.py               # Batch API map pass → records
│   ├── embed_assign.py          # assign to clusters, update baselines
│   └── alert_check.py           # threshold logic → alerts.json
├── state/                       # committed: the radar's memory
│   ├── clusters.json            # stable IDs, centroids, names, birth dates
│   ├── baselines.json           # per-cluster daily volume stats, day-of-week aware
│   └── watermark.json           # last processed ticket timestamp
├── data/                        # gitignored: raw tickets, records, embeddings
└── runs/                        # committed: one folder per day
    └── 2026-07-06/
        ├── cluster_summaries/
        ├── alerts.json
        └── brief.md

This is the anchor post's layout with one addition that carries the whole alerting mission: state/baselines.json. For every cluster it stores recent daily volumes and day-of-week statistics, because Monday and Saturday are different planets in a support queue. The cluster identity rules from the anchor post go into CLAUDE.md verbatim: IDs are stable, never renumbered; new tickets join clusters by embedding distance; only distance beyond threshold creates a candidate.


Phase 2: Getting tickets out (Intercom vs Zendesk)

The two big helpdesks need different handling, and pretending otherwise wastes your first week.

Intercom has an official MCP server at mcp.intercom.com/mcp (US-hosted workspaces only at the time of writing; check yours first). Even so, use it for drill-downs, not bulk export. export_tickets.py should hit Intercom's REST API directly with an access token: pull conversations updated since the watermark, paginate, flatten each into one text blob (customer messages plus resolution notes), strip PII per your Phase 0 decision, write to parquet.

Zendesk has no official MCP server. There is a community one, reminia/zendesk-mcp-server; evaluate it like any third-party code before pointing it at customer data. For the pipeline it doesn't matter: bulk export goes through Zendesk's REST APIs (their incremental export endpoints exist for exactly this) in the same script, with the same watermark pattern. If you adopt the community MCP at all, scope it to interactive drill-downs ("show me ticket 48219") during analysis sessions.

The rule generalizes: MCP is how agents ask targeted questions; scripts are how data moves in bulk. Claude Code writes export_tickets.py in a normal interactive session; expect real back-and-forth on pagination and field mapping, because helpdesk APIs are crufty.


Phase 3: The extraction schema for tickets

extract.py runs each new ticket through a small model via the Batch API (roughly 50% discount, and nothing here is latency-sensitive). The schema is where tickets differ from product conversations: you are extracting for triage, so severity signals get first-class fields.

{
  "ticket_id": "48219",
  "schema_version": 3,
  "product_area": "exports",
  "symptom": "CSV download returns empty file",
  "user_intent": "export monthly report",
  "severity_signals": ["user_blocked", "mentions_deadline"],
  "sentiment": -0.7,
  "plan_tier": "pro",
  "mentions_recent_change": true,
  "representative_quote": "this worked fine until yesterday",
  "quote_is_verbatim": true
}

Two fields earn their keep daily. severity_signals is a controlled vocabulary (user_blocked, data_loss_mention, refund_or_cancel_threat, security_mention, mentions_deadline); a new cluster where half the tickets carry user_blocked is a different alert than one full of how-do-I questions. mentions_recent_change ("since the update", "after yesterday") is your cheapest release-regression detector, and it costs one boolean.

The anchor post's three extraction rules apply unchanged: use the cheapest model that passes a 50-ticket hand-graded spot check, force verbatim quotes with the flag the critic will verify, and version the schema.


Phase 4: Clustering, baselines, and the alert rule

embed_assign.py is the anchor post's assignment loop: embed each record's symptom, product area, and quote; assign within threshold to an existing cluster; hold out the rest; run HDBSCAN over holdouts to form candidate clusters. Monthly re-clustering with ID remapping applies here too.

What's new is alert_check.py, and its central design decision: volume above baseline beats absolute counts. A fixed rule like "alert on any candidate cluster with 10+ tickets" fails twice. On a 600-ticket Monday it fires on noise; on a 150-ticket Sunday a real outage generating 8 tickets sails under it. And as your product grows, the fixed number silently loosens. So both alert conditions are relative:

  • New-cluster alert: a candidate cluster fires when it contains at least max(5, 2% of the day's ticket volume) tickets. The floor of 5 keeps tiny days from alerting on coincidence; the percentage scales with you.
  • Spike alert: a known cluster fires when today's volume exceeds its same-weekday baseline mean by more than 3 standard deviations, with a small absolute floor for near-zero baselines.

Start with those numbers, expect to tune them within two weeks, and log the active thresholds into every run's alerts.json so that when someone asks "why didn't this alert Tuesday," the answer is in git.


Phase 5: The agents

The fan-out is smaller than the anchor post's because the radar only investigates what alerts. Three roles:

cluster-analyst.md gets one alerting cluster: its summary, its baseline history, and 5 exemplar tickets in full (the only place raw tickets enter a context). It answers: what exactly is failing, is this plausibly tied to a release (check mentions_recent_change density and the cluster's birth date against your changelog), how severe per the signals, and what should the alert tell an on-call engineer.

synthesizer.md writes the daily brief: alerts first with analyst findings, then the quiet one-line summary of known clusters, then candidates below threshold as a watch list.

critic.md is the judge loop, and at daily cadence with loud alerts it is not optional. The checklist, concretely:

1. Every quote in the brief: grep it verbatim against the exemplar
   files. Not found → FAIL.
2. Every "NEW cluster" claim: verify the cluster ID is absent from
   yesterday's clusters.json in git history. Present → FAIL.
3. Every spike claim: recompute today's count and the baseline stats
   from cluster_summaries and baselines.json. Math off → FAIL.
4. Is the "new" cluster actually a known cluster reworded? Check
   centroid distance to its nearest neighbor; if borderline, say so
   in the alert instead of claiming novelty.
5. If alerts.json is empty, the brief must be one line. A padded
   quiet-day brief trains people to skim → FAIL.
Return PASS or FAIL with fixes. Max 2 revision loops.

A false "NEW" alert costs you the team's trust in exactly one incident channel. The critic is how you spend tokens to protect that.


Phase 6: Scheduling it daily

Every scheduled run is an individual run: tomorrow's session does not arrive knowing today's conclusions. Continuity is engineered, exactly as in the anchor post, by committing state/ and making "read prior state" step 1 of the skill.

Managed path: cloud routines via /schedule (or claude.ai/code/routines). The minimum interval is one hour, so daily at 6am is comfortably supported. The routine clones your repo's default branch, loads .claude/ and .mcp.json, runs /ticket-radar autonomously, and pushes commits on a claude/ prefixed branch. Daily cadence sharpens one consequence that weekly workflows can shrug off: if runs land on branches and nobody merges, tomorrow's run clones a default branch without today's state. Yesterday's new cluster becomes "new" again every morning. Either enable direct push for this repo (defensible for a state-and-reports repo) or automate the merge; deciding is not optional. Configure the extraction API key and helpdesk token in the routine's environment variables; routine runs draw on your plan's usage daily, so watch /usage the first week.

Self-hosted path: system cron plus headless mode on a machine you own: claude -p "/ticket-radar" --output-format json with --allowedTools scoped to the pipeline. You control secrets and data flow; you also own the 6am failure when the machine is asleep. At daily cadence, that tradeoff bites 7x harder than weekly.

Run-status alerting is separate from the radar's own alerts: a Stop hook in .claude/settings.json posts every run's completion status to Slack. A radar that silently died Tuesday is worse than no radar, because everyone assumes it's still watching.


Phase 7: Sharing with the team

Three channels, in escalating order of commitment:

  • Slack is the primary surface. The skill's final step sends the alert (or the one-line all-quiet) via a plain webhook script. There is no official Slack MCP server; the community option is korotovsky/slack-mcp-server, worth evaluating for richer interaction, but a webhook is enough for delivery. The division of labor: the Stop hook reports that the run happened; this step carries what it found.
  • The daily brief as a published artifact. Claude Code can publish brief.md as a hosted page on claude.ai with a stable URL. Caveats to plan around: publishing works from the CLI and desktop app, not from every automated surface, and organization-scoped visibility requires a Team or Enterprise plan. In a fully autonomous daily setup, treat the artifact as the human-refreshed archive and Slack as the wire.
  • Confirmed clusters become Linear tickets. Linear has an official MCP server (mcp.linear.app/mcp); add it to .mcp.json and make ticket creation a skill step that runs only for human-confirmed clusters: title from the cluster name, body with volume, baseline context, severity signals, and one verbatim quote. Auto-filing unconfirmed candidates floods the tracker and burns the goodwill this system runs on; the confirmation is a one-line edit to clusters.json in a PR, same as the anchor post.

Phase 8: Maintenance (what you own now)

The anchor post's list applies in full: threshold tuning, monthly re-cluster reviews, quarterly extraction audits, schema migrations, candidate hygiene. Daily cadence adds its own:

  • Alert fatigue reviews. Once a month, count alerts fired versus alerts acted on. Below roughly half acted on, raise thresholds; the radar's value is people not muting it.
  • Helpdesk API drift. Intercom and Zendesk both evolve their APIs; export_tickets.py should validate expected fields and refuse to run the pipeline on malformed exports rather than clustering garbage quietly.
  • Product taxonomy drift. New features mean new product_area values in the extraction prompt; stale vocabulary shows up as a mushy "other" cluster growing week over week.
  • The merge duty. If you chose branch-per-run over direct push, someone merges daily. Name that person.

What this costs

Assumptions repeated: 200-600 tickets/day at 500-1,000 tokens each, so 1-4 million extraction input tokens per week.

  • Extraction: the anchor post lands in the tens of dollars weekly for roughly 27M tokens through a small model on the Batch API; this workflow pushes about a tenth of that volume, so expect low single-digit dollars per week at the same rates. Real metered spend, but small, and linear in ticket volume.
  • Embeddings: effectively zero with a local model at this scale.
  • The daily analysis session: most days it reads summaries, finds nothing alerting, and writes one line; alert days fan out a few analysts over exemplars. Both fit within subscription usage; watch the /usage trend in week one, since daily runs compound in a way weekly ones don't.
  • Plan tier: routines need Pro and up; organization-scoped artifact sharing needs Team or Enterprise.
  • The unpriced line: someone tunes two thresholds, confirms candidate clusters, reviews the monthly re-cluster, and merges the daily branch. Smaller than the conversation intelligence system's human footprint, but a standing duty with a name on it.

The four practical challenges

  1. Setup at scale: the three-layer architecture inherited from the anchor playbook, plus the baseline state and threshold logic that turn clustering into a radar (Phases 1-4).
  2. Sharing with the team: Slack as the wire, artifacts as the archive with their CLI/desktop and plan-tier caveats, and Linear routing for confirmed clusters (Phase 7).
  3. Maintenance: everything the anchor post owns, plus alert-fatigue reviews, helpdesk API drift, and the daily merge duty (Phase 8).
  4. Cost: low single-digit dollars weekly of metered extraction on stated assumptions, subscription usage that compounds daily, and a human threshold-tuner no invoice shows (What this costs).

The managed alternative

Helpdesk vendors sell parts of this natively: Zendesk's AI triage and Intercom's Fin-era reporting will categorize and summarize tickets inside their own dashboards, and feedback-analytics platforms like Thematic cluster support text across sources. The strengths are real: zero pipeline to build and categories out of the box. The trades: you see tickets through the vendor's taxonomy rather than clusters that emerge from your data, the "new issue that didn't exist yesterday" alarm is only as good as their anomaly logic, and if your tickets span tools, the helpdesk-native option only sees its own half.

Doing this with Second Axis

Everything above is the do-it-yourself path. On Second Axis, the ticket radar is a workflow in the marketplace: connect Zendesk or Intercom, choose the workflow, and set the goal ("daily emerging-issue alerts to the support channel, weekly digest to product"). It runs every night with the clustering, baseline diffing, and alert thresholds built in, and it monitors itself: if the helpdesk export breaks or a run fails, it finds the error and alerts you, which matters most in exactly the system whose job is to never miss a spike.