← All workflow playbooks

A Weekly Voice-of-Customer Rollup with Claude Code, Step by Step

Date: July 6, 2026 • Author: Second Axis

Your customers are complaining about the same export bug in five different places this week. One filed a support ticket. Two mentioned it to a CSM on a call, where it lives in a Gong transcript nobody rereads. Four grumbled in your community Slack. One left a G2 review. Each channel's owner saw their fragment, nobody saw five, and your loudest signal of the week arrived as background noise in five inboxes.

The fix is a weekly voice-of-customer rollup: every complaint, request, and quote from every channel, deduplicated, clustered by theme, weighted by who said it, diffed against last week, and routed to the product person who owns that area. Can you build that with Claude Code? Yes. But the interesting part is not the model reading feedback; it is the plumbing that gets five differently shaped sources into one shape the model can reason about. This post is the complete build, with all the engineering visible.

One thing to read first: this workflow shares its core machinery with the conversation intelligence playbook, the anchor for every text-at-scale build. Its three layers (a cheap-model extraction pass, embedding clustering with stable IDs, analyst agents that only read cluster summaries) apply here unchanged, so this post will not re-derive them. What fills this playbook is everything that comes from having five sources instead of one: per-source extractors, schema normalization, cross-channel dedupe, channel weighting, and theme ownership.


What you're building

Every Monday morning, without you touching anything:

  • A ranked theme list across all channels: "CSV export produces empty files" as one theme with 23 mentions, not five disconnected fragments.
  • Channel breadth per theme: a complaint appearing in tickets, calls, and reviews simultaneously outranks one loud in a single channel, because breadth is evidence it is real.
  • Weighted and raw counts side by side: one enterprise account on a QBR call is not the same signal as 50 free users in Slack, and the report shows both readings instead of silently picking one.
  • Verbatim quotes with sources, so every theme comes with the actual words customers used and a link back to the ticket, call, or review.
  • A diff against last week's theme state: new themes flagged loudly, known themes shown as trend lines, resolved themes closed out.
  • An owner on every theme, routed by product area, so the rollup produces accountability rather than ambient awareness.

The scale math

Stated assumptions for a mid-size B2B product; substitute your own numbers:

  • 800 support tickets a week, averaging 800 tokens of customer text each: about 640K tokens.
  • 60 sales and CS call transcripts a week at roughly 10K tokens each: about 600K tokens.
  • 2,000 community Slack messages at 60 tokens each: about 120K tokens.
  • 50 G2 and app store reviews at 250 tokens each: about 12K tokens.

Call it 1.4 million tokens of raw customer text per week, roughly seven times a 200K context window, every week, forever. That is smaller than the conversation-intelligence firehose (about 27M tokens a week in that playbook's assumptions), so extraction cost will be modest. But the context math still rules out "paste it all into one prompt," and the harder constraint is shape, not volume: five sources with five schemas, five auth models, and five notions of what a "customer" is. The architecture is the anchor post's three layers, plus a normalization layer in front and a dedupe layer between extraction and clustering. Those two additions are this post.


Phase 0: Prerequisites, and the per-source access reality

Beyond the standard baseline (Claude Code on a Pro, Max, Team, or Enterprise plan; a separate metered API key for the extraction pass; Python 3.11+ with duckdb, pandas, scikit-learn, and an embeddings model; a dedicated git repo), the real prerequisite work is access, and it is uneven across sources. As of July 2026:

  • Intercom: official MCP server at mcp.intercom.com/mcp, currently US workspaces only. The best-supported source on this list.
  • Zendesk: no official MCP server. There is a community option (reminia/zendesk-mcp-server), or a plain REST export script, which for bulk weekly pulls is the more dependable path anyway.
  • Slack: no official MCP server either. The community korotovsky/slack-mcp-server (around 1.7k stars) exists; a scoped bot token plus the conversations API in a script is the alternative. Either way, a Slack admin has to approve the app and pick which channels it can read.
  • Call transcripts: community servers exist for Gong (kenazk/gong-mcp) and Fireflies (Props-Labs/fireflies-mcp). Community means you audit the code and you own it when the vendor API shifts underneath it.
  • Reviews: app store reviews have a community server (JiantaoFu/AppInsightMCP). G2 has no server worth citing; plan on its export tooling plus a small parsing script.

The pattern to internalize: MCP servers are for interactive drill-downs during analysis ("show me that ticket"). The weekly bulk pull should be boring scripts hitting REST APIs with service credentials, because scripts are testable, retryable, and consume no model context. Also count the humans: five sources means five credential approvals from five tool admins, and that calendar time is usually the longest pole in week one.


Phase 1: The repo and state layout

voc-rollup/
├── CLAUDE.md
├── .claude/
│   ├── agents/
│   │   ├── theme-analyst.md
│   │   ├── synthesizer.md
│   │   └── critic.md
│   └── skills/
│       └── voc-rollup/
│           └── SKILL.md
├── scripts/
│   ├── extract_intercom.py       # one extractor per source
│   ├── extract_gong.py
│   ├── extract_slack.py
│   ├── extract_reviews.py
│   ├── normalize.py              # everything → feedback_records.parquet
│   ├── dedupe.py                 # cross-channel duplicate collapse
│   └── cluster.py                # assign records to themes
├── config/
│   ├── weights.yaml              # channel and account-tier weights
│   └── owners.yaml               # theme → product area owner
├── state/                        # committed: the system's memory
│   ├── themes.json               # stable theme IDs, centroids, names, owners
│   └── watermarks.json           # last-processed timestamp PER SOURCE
├── data/                         # gitignored: raw pulls, embeddings
└── runs/                         # committed: weekly rollups + snapshots

Two state files carry the recurring design, same mechanism as the anchor post with one multi-source twist: watermarks.json holds a separate high-water mark per source, because Slack will be current while the Gong pull is three days behind, and a global watermark would silently drop data. themes.json is the theme registry with stable IDs; its identity rules (never renumber, assign by embedding distance, candidates need human confirmation, periodic full recluster with ID remapping) are exactly the anchor post's cluster registry, so follow that playbook's Phase 3 verbatim.

CLAUDE.md carries the standing rules: never read raw source dumps into context, work only from runs/<date>/ summaries, always diff against the previous run, never edit config/ or scripts/ during a scheduled run.


Phase 2: Per-source extractors and the one record format

This phase makes or breaks the system, and it is almost entirely schema work. Each extractor pulls its source's new items since the watermark and hands off raw text plus source metadata. Then normalize.py, with an extraction pass behind it for the unstructured sources, converts everything into one shared record format:

{
  "record_id": "intercom-88213-c2",
  "source": "intercom",
  "captured_at": "2026-07-02T14:11:00Z",
  "customer_ref": "acct_4411",
  "account_tier": "enterprise",
  "speaker_role": "customer",
  "kind": "complaint",
  "summary": "CSV export completes but the downloaded file is empty",
  "verbatim_quote": "the export finishes but the file has literally nothing in it",
  "quote_is_verbatim": true,
  "product_area_guess": "exports",
  "severity_signal": "churn_threat",
  "schema_version": 3
}

Why this is the real engineering: each source resists a different field. A ticket is one customer speaking, but a Gong transcript is a mixed conversation where most sentences are your own rep talking; the call extractor must attribute speakers and keep only customer statements, or your rollup will faithfully cluster your own sales pitch. Slack messages need thread reconstruction, because "same here!!" is meaningless without its parent. Reviews have ratings but no reliable account identity. customer_ref and account_tier come from a CRM join for tickets and calls, and are often simply unknowable for Slack handles and anonymous reviews, so the schema must tolerate nulls rather than pretend.

The extraction itself follows the anchor post's rules exactly: cheapest model that passes a 50-sample hand grading, Batch API since nothing is latency-sensitive, quote_is_verbatim enforced, schema_version on every record. Claude Code writes all five extractors and the normalizer; expect a real interactive session per source, because each fails differently (pagination quirks, rate limits, transcript formats) and you want those failures found while a human is watching.


Phase 3: Cross-channel dedupe, then clustering

Here is why naive text matching dies. The same export bug arrives as "CSV export is broken," "downloaded the report and got a blank file," and a two-minute rant on a call that never uses the word export at all. String similarity across those is near zero. Deduplication has to happen in meaning space: dedupe.py embeds each record's summary and quote, then collapses at two levels.

Level one, same-voice duplicates: the same customer saying the same thing in multiple places (a ticket, a Slack post, a mention on a call). Where customer_ref is known, records from one account within a window whose embeddings sit above a high similarity threshold merge into one record with multiple source links. This matters for counting: one angry customer using three channels is one data point with high intensity, not three.

Level two, same-theme grouping: distinct customers saying the same thing is not a duplicate, it is the signal, so those records stay separate but flow into cluster.py for theme assignment against themes.json, using the anchor post's incremental-assignment mechanics.

Two decisions live in config/weights.yaml and deserve a named human owner, because they are judgment, not math:

  • Channel and tier weighting. Does one enterprise account's complaint on a QBR call outweigh 50 free users in Slack? There is no universal answer, only your revenue model. Encode it as explicit multipliers, and always show raw counts next to weighted scores so the weighting can be argued with instead of hidden.
  • Theme ownership. config/owners.yaml maps product areas to owners; cluster.py stamps each theme with an owner from its dominant product_area_guess, and themes with no confident area route to a triage owner. A theme without an owner in this system is a bug, and the critic treats it as one.

Phase 4: The agents

The analyst, synthesizer, critic pattern carries over from the anchor post; the specialization is per theme. theme-analyst.md receives one theme's weekly summary plus up to five exemplar records with full source context, and answers: what exactly are customers experiencing, which segments and channels, is it growing, and what is the workaround today? The synthesizer ranks themes by weighted score and breadth, writes the "new this week" and "changed since last week" sections against the prior snapshot, and drafts the rollup grouped by owner.

The critic's checklist is where multi-source trust is enforced:

1. Every quote in the rollup: locate it verbatim in the exemplar
   records, and confirm the cited source link matches. A quote that
   was paraphrased, or attributed to the wrong channel, is a FAIL.
2. Every "new theme" claim: confirm the ID is absent from last
   week's snapshot in runs/.
3. Every count: recompute raw and weighted from this run's
   summaries; both must appear in the report.
4. Every theme has an owner, or is explicitly listed under triage.
5. Any theme drawing from only one source is labeled single-channel,
   never presented as company-wide sentiment.
Return PASS, or FAIL with specific fixes. Max two revision loops.

Check 1 matters most. A voice-of-customer report earns its authority from real customer words; the first invented quote an exec traces back to nothing kills the program.


Phase 5: Scheduling

The mechanics are the anchor post's, so briefly, with the multi-source wrinkles. Every scheduled run is an individual run: a fresh session with no memory of last Monday. Continuity is the thing you engineered in Phase 1, committed state read as step one. The /voc-rollup skill reads state/ and last week's run, executes the extractors, normalization, dedupe, and clustering, fans out theme analysts, synthesizes, critiques, persists the new snapshot, and commits.

The managed path is a weekly cloud routine created with /schedule (or at claude.ai/code/routines). The routine clones the repo's default branch, which is how it inherits state/, runs autonomously, and pushes to a claude/ prefixed branch; runs draw on your plan's usage. The multi-source catch is secrets: five credentials configured in the routine's environment, and any one expiring takes down one fifth of your input, so the skill must treat a failed extractor as a loud per-source warning in the report, never a silent gap. The self-hosted alternative is system cron running headless mode, claude -p "/voc-rollup" with --allowedTools scoped to the pipeline, on a machine whose uptime you now own. Either way, add a Stop hook posting run status to Slack, because a rollup that silently died is worse than none.


Phase 6: Sharing with the team

  • Artifact publish for the weekly rollup page: a stable URL, organization-scoped visibility on Team and Enterprise plans, viewers just sign in to claude.ai. The standing caveat: publishing works from the CLI and desktop app, not from every automated surface, so in a fully autonomous setup the publish step may be where a human stays in the loop.
  • A Slack digest that is deliberately not the whole report: top three movers, any new themes, and each owner tagged next to their themes with a link to the full page. Owner tagging is the difference between a digest people scroll past and one that starts threads.

Phase 7: Maintenance (what you own now)

Everything from the anchor post's list applies (recluster reviews, extraction audits, schema migrations, candidate hygiene). Multiplying sources multiplies the rest:

  • Five APIs that break independently. Intercom changes a field, Gong tightens a rate limit, a Slack scope gets deprecated, the review format shifts. Each extractor needs a validation step (expected volume within historical range) so a source failing reads as an alert, not as "customers got quiet."
  • Five credentials that rotate independently, each living in the routine's environment and each owned by a different tool admin. Keep a rotation calendar or learn each expiry date the hard way.
  • Dedupe threshold tuning. Too loose and distinct complaints merge into mush; too tight and one customer's three channels count as three voices. Log the thresholds into every run snapshot so trend breaks are explainable.
  • Theme taxonomy drift. Product areas reorganize, owners change teams, owners.yaml quietly rots. Review it monthly; an orphaned theme routes to nobody, which is the exact failure this system exists to prevent.

What this costs

Transparent assumptions, per the scale math: about 1.4M tokens of customer text weekly across five sources.

  • Extraction: roughly 1.4M input tokens a week through the cheapest model that passes your spot checks, via the Batch API at its roughly 50% discount. That is about one twentieth of the anchor playbook's 27M-token extraction volume, so the line item that was tens of dollars a week there is proportionally small here. It is metered API spend on top of your subscription, scaling linearly with feedback volume; check current published rates against your own numbers rather than trusting anyone's total.
  • Embeddings and dedupe: negligible hosted, zero marginal cost with a local sentence-transformers model.
  • The analysis session: theme summaries plus a handful of exemplar-reading analysts, within subscription usage. The usual discipline applies: agents on rails, reading summaries, never wandering into data/.
  • Plan tier: cloud routines need Pro and up; organization-scoped artifact sharing needs Team or Enterprise.
  • The real invoice: five extractors to build and babysit, a normalization schema that is really a data model of your customer feedback, weighting decisions someone must defend, an ownership map someone must keep current. The token bill is the small line; the standing data-engineering duty is the big one.

The four practical challenges

  1. Setup at scale: not raw volume this time, but shape. Five sources, five schemas, five auth models, all normalized into one record format before the anchor architecture (cheap extraction, embedding clusters, agents on summaries) can even start. The normalizer is the system.
  2. Sharing with the team: artifact publish plus an owner-tagged Slack digest, with the standing caveats that publishing runs from CLI and desktop only and org-wide visibility needs Team or Enterprise.
  3. Maintenance: everything a single-source text pipeline needs, times five, plus dedupe thresholds and an ownership map that both drift with your product. Budget a recurring human hour, not a one-time setup.
  4. Cost: modest metered extraction spend at this volume, cheap embeddings, subscription-covered analysis. The dominant cost is the engineering and the standing ownership, and it never appears in /usage.

The managed alternative

Voice-of-customer is the most mature managed category in this series: Enterpret, Thematic, and Chattermill all ingest feedback from multiple sources and cluster it by theme. If any workflow in this series justifies buying instead of building, it's this one. The trades are still worth naming: enterprise pricing, your feedback corpus living in another vendor's system, taxonomy tuning that remains your job, and a fixed connector list. If a source they don't support matters to you (a community forum, a niche channel), you're back to building the extractor anyway.

Doing this with Second Axis

Everything above is the do-it-yourself path. On Second Axis, the VoC rollup is a workflow in the marketplace: connect your helpdesk, call recorder, Slack, and review sources, choose the workflow, and set the goal ("weekly deduped voice-of-customer rollup by theme, with week-over-week diffs"). It runs on schedule with the per-source extraction, cross-channel dedupe, and theme memory handled, and it monitors itself: a broken source connection or a failed run gets found and alerted, so a silent gap in one channel doesn't quietly skew a month of rollups.