/voc-analyze-calls — Call Intelligence Report
Reads the transcripts /voc-source-calls wrote and produces one report answering: who is
showing up, what hurts, what stops them buying, who else they are looking at, and what they
asked for that you do not have.
The orchestration matters. A single context window cannot hold 60 transcripts at 40-80KB each, and summarizing them one at a time loses exactly the cross-call patterns you are looking for. So: classify cheaply in the main thread, fan out to subagents that each read a small batch in full, and consolidate structured JSON back in the main thread.
Requires
| What | Value |
|---|---|
| Input | outputs/calls/index-*.json and transcripts, from /voc-source-calls |
| Config | config/thresholds.json, context/product-context.md |
| Cost | Subagent tokens only. A 60-call week runs ~8-10 subagents. |
Phase 1 — Select the calls worth reading
1.1 Load the index
Glob outputs/calls/index-*.json, pick the most recent, parse it. Parse the JSON, not the
markdown table. Assert that the number of rows you parsed equals calls.length in the file;
if it does not, stop and say so rather than analyzing a partial set.
1.2 Duration filter
Already applied by the fetcher using call_filters.min_duration_seconds. Report the count that
was dropped so the reader knows the denominator.
1.3 First pass — classify by title
Title patterns are free to evaluate and catch most internal and customer-success calls. Classify as CUSTOMER (excluded from this report) when the title contains any of:
weekly · biweekly · daily · standup · catch up / catchup / catch-up ·
check in / check-in · onboarding · kickoff / kick-off · setup / set up ·
training · QBR · dry run · rehearsal · support · escalation · renewal ·
retro / retrospective · sync · internal
Add your own patterns to context/product-context.md under customer_call_patterns — every
company has house naming conventions, and the ones above are only a starting set.
Everything else stays UNCLASSIFIED.
Edge case worth handling: follow-up and next steps appear in both sales and delivery
contexts. Leave them unclassified and let the second pass decide.
1.4 Second pass — classify by content
For each UNCLASSIFIED call, read only the Metadata, Participants, Summary and Key Points
sections of the transcript file. Stop at ## Topics. That is roughly the first 60-80 lines and
it is enough.
| Verdict | Signal |
|---|---|
| PROSPECT | Discovery, demo, pricing discussion, security review, "what does your product do", competitive comparison, procurement, first-time exploration |
| CUSTOMER | Existing implementation, configuration, an event or project already contracted, support, expansion into a new team |
| INTERNAL | Every participant is internal. Excluded and counted separately. |
| UNREADABLE | No summary, no key points, no transcript. Counted, not analyzed, and reported. |
Anything genuinely ambiguous goes to PROSPECT. A customer call misread as a prospect call adds a little noise; a prospect call thrown away loses the signal you ran this for.
1.5 Batch
Split the PROSPECT list into batches of 6-8 transcripts. That fits a subagent context with
room to reason. Cap total subagents at cost_guards.max_parallel_subagents; if the prospect
count would exceed it, raise batch size instead of dropping calls, and say in the report that
you did — silent truncation reads as full coverage.
Record the funnel: total in index → filtered short → internal → customer → unreadable → prospect.
Phase 2 — Fan out
Launch every subagent in a single message. Sequential launches waste the entire benefit of this architecture.
Give each subagent this prompt, with the batch's absolute file paths substituted:
You are analyzing customer-facing sales call transcripts. Read each of these files in full:
[ABSOLUTE FILE PATHS]
Aggregate across all calls in your batch. Do not summarize calls individually.
CRITICAL RULE: Only extract quotes and pain points from speakers marked [external].
Speakers marked [internal] are the sales team — their words are the pitch, not the
voice of the customer. If a transcript has no speaker labels, extract nothing from
it and list it under `unattributable`.
Return ONLY valid JSON, no prose before or after:
{
"batch_id": 1,
"calls_analyzed": ["title", ...],
"unattributable": ["title of any call with no speaker labels", ...],
"personas": [
{"role": "...", "seniority": "IC|manager|director|vp|c-level",
"org_type": "...", "industry": "...", "count": 1}
],
"use_cases": [{"job_to_be_done": "...", "trigger": "...", "count": 1}],
"pain_points": [
{"pain": "...", "count": 1, "severity_signal": "blocker|friction|annoyance",
"quote": "...", "company": "...", "date": "YYYY-MM-DD"}
],
"objections": [
{"objection": "...", "category": "price|timing|features|security|procurement|incumbent|trust",
"count": 1, "how_rep_handled": "...", "landed": true}
],
"competitive_mentions": [
{"competitor": "...", "context": "incumbent|evaluating|switched_from|dismissed",
"sentiment": "positive|negative|neutral", "what_was_said": "...", "count": 1}
],
"feature_requests": [
{"request": "...", "count": 1, "blocking_deal": true, "verbatim": "..."}
],
"pricing_signals": [
{"signal": "...", "budget_mentioned": "...", "reaction": "sticker_shock|neutral|value_accepted"}
],
"quotes": [
{"quote": "exact verbatim, external speaker only", "speaker_role": "...",
"company": "...", "date": "YYYY-MM-DD", "why_notable": "..."}
]
}
Extraction guidance:
- pain_points: listen for "we currently", "the problem is", "what happens today is",
"we've been doing this manually", "our current tool can't". Distinguish a blocker
(deal dies without it) from friction (annoying, survivable).
- objections: capture whether the rep's answer actually landed. An objection that
recurs after being handled is a messaging failure, not a sales failure — that
distinction is the most useful thing in this whole report.
- competitive_mentions: `context` matters more than sentiment. "We're leaving Acme"
and "We're also looking at Acme" are different intelligence.
- feature_requests: only things asked for that were NOT clearly available. A question
about an existing feature is a discoverability problem, not a request — note those
under pain_points instead.
- quotes: 2-4 per batch. Prefer quotes that reveal motivation, decision criteria, or
a memorable objection over quotes that merely praise.
- If a call is a gatekeeper screen, a reschedule, or the prospect barely speaks,
include the title in calls_analyzed with a "(low content)" suffix and extract nothing.
Wait for all subagents before Phase 3.
Phase 3 — Consolidate
Merge the JSON. Rules that matter:
- Group before you count. "Registration is manual" and "no self-serve signup" are one pain. Merge on meaning, sum the counts, keep the strongest quote.
- Never invent a count. If three subagents each report a pain once, it is 3, not "common".
- Sort by count descending, then by severity. A blocker mentioned twice outranks an annoyance mentioned five times — note both numbers rather than collapsing them.
- Recurring handled objections get flagged. Any objection where
landed: falseappears more than once is a messaging gap and belongs at the top of the report, not buried in a list. - Deduplicate quotes by company plus first eight words.
Output
Write to outputs/calls/analysis/call-intelligence-<YYYY-MM-DD>.md.
# Call Intelligence — week of <range>
## Coverage
| Stage | Count |
|---|---|
| Calls in index | N |
| Filtered: under <N>s | N |
| Excluded: internal only | N |
| Excluded: existing customers | N |
| Unreadable (no transcript or summary) | N |
| **Analyzed as prospect calls** | **N** |
| Subagents used | N |
<If anything was truncated or skipped, state it here in plain language.>
## The five things worth acting on
<Five bullets. Each names the pattern, the count, and what to do. Written for someone
who will read only this section. No preamble.>
## Who showed up
| Role | Seniority | Org type | Calls |
|---|---|---|---|
## What hurts
| # | Pain | Calls | Severity | Representative quote |
|---|---|---|---|---|
## What stopped them
| Objection | Category | Calls | Handled? | Note |
|---|---|---|---|---|
**Recurring after being handled:** <these are messaging gaps, list them explicitly>
## Who else they are looking at
| Competitor | Calls | Context | What was actually said |
|---|---|---|---|
## What they asked for that we do not have
| Request | Calls | Blocking a deal? | Verbatim |
|---|---|---|---|
## Pricing reactions
<Only if there is signal. Do not pad.>
## Quotes
> "verbatim"
> — Role, Company, YYYY-MM-DD · *why this is notable*
## What this report cannot tell you
<Honest limits for this specific run: how many calls had no speaker labels, whether
one large deal dominates the sample, which sources were unavailable.>
What this gets wrong
State these in the report when they apply. They are the difference between intelligence and a confident-sounding average.
- Sales calls are a biased sample. You hear from people who took a meeting. The ones who bounced off your pricing page are invisible here. Never describe these findings as "what the market thinks."
- Counts are mention counts, not prevalence. Five mentions of a pain in a week of 40 calls is a signal to investigate, not a 12.5% incidence rate.
- Reps lead witnesses. If every call surfaces the same pain, check whether the discovery script asks about it directly before treating it as organic.
- Unlabelled speakers poison the well. If a meaningful share of transcripts came back with no participant list, your quotes may include your own team. Report the number.
- One loud account can dominate. If a single company accounts for several calls, its concerns will look like a trend. Check the company spread before believing a top-ranked pain.