/voc-source-custom — Bring Your Own Source
The pipeline does not care where a signal came from. It cares that the signal arrives in a known shape with honest metadata. This skill is the on-ramp for everything the dedicated source skills do not cover.
Common uses: NPS and survey open-text, support tickets, CRM email threads, community and forum posts, churn exit interviews, app store reviews, sales-lost reason fields, win/loss interviews.
Requires
| What | Value |
|---|---|
| Access | An MCP server, an API you can call from Bash, or an export file |
| Config | context/product-context.md → custom_sources |
| Cost | Depends on the source. Most are free. |
The contract
Every signal file written anywhere under outputs/voc/sources/ must have this shape. Get this
right and everything downstream works without modification.
{
"channel": "survey",
"verbatim": "exact text as the customer wrote or said it",
"fidelity": "verbatim",
"company": "Northwind Trading",
"speaker_role": "Operations Director",
"date": "2026-04-22",
"signal_type": "pain",
"source_link": "https://...",
"source_file": "outputs/voc/sources/survey/raw-2026-04-22.json",
"notes": "NPS detractor, score 4"
}
| Field | Rule |
|---|---|
channel |
A new, distinct name. This is what the confirmation rule counts. Do not reuse an existing channel name for a different source — and do not invent two names for the same source, which manufactures false corroboration. |
verbatim |
Exact. No cleanup. |
fidelity |
verbatim or summarized. Be honest; /voc-dispatch gates testimonial use on it. |
company |
null if unknown. Becomes tier U. That is fine. |
date |
When the customer said it, not when you exported it. |
signal_type |
pain | request | praise | objection | competitive | churn_reason |
Execution
Step 1 — Register the source
Add it to custom_sources in your context file:
custom_sources:
- name: nps
channel: survey
method: api # api | mcp | file
endpoint: "https://api.example.com/responses"
auth_env: SURVEY_API_KEY
text_field: comment
date_field: submitted_at
filter: "score <= 6"
Step 2 — Pull
| Method | How |
|---|---|
file |
Glob the export directory and read. Simplest, works for anything with CSV or JSON export. Start here. |
api |
A small script under scripts/sources/. Reuse the throttle and retry pattern from fetch-calls.js rather than writing another one. |
mcp |
Run /mcp first, confirm the exact tool names your server exposes, then add them to this file's allowed-tools. |
Step 3 — Filter aggressively
Most custom sources are mostly noise. Support tickets are overwhelmingly password resets. Survey open-text is largely blank or one word.
Drop, and report the counts:
- Responses under about 15 characters, or with no verb
- Tickets resolved as duplicate, spam, or user error
- Anything from an internal email domain
- Anything already captured by another source (see deduplication below)
Report what you dropped and why. 842 responses → 61 with usable open text is a useful line;
silently reporting 61 is misleading.
Step 4 — Classify signal type
Cheap heuristics first, model judgment only for the remainder:
| Type | Signals |
|---|---|
pain |
"can't", "doesn't work", "takes too long", "have to manually" |
request |
"wish", "would love", "need the ability to", "please add" |
praise |
"love", "saved us", "best part", "exactly what we needed" |
objection |
"too expensive", "not sure it's worth", "waiting until" |
competitive |
any named vendor |
churn_reason |
present in exit interviews and lost-deal fields |
Step 5 — Deduplicate against existing channels
Before writing, check whether this signal already exists under another channel. A support ticket that a rep also pasted into chat, then discussed on a call, is one customer complaint — not three independent sources.
Match on company plus a date window of a few days plus text similarity. When you find a duplicate, keep the highest-fidelity version and drop the rest.
This check is the difference between a pipeline that finds patterns and one that manufactures them. Nothing downstream can catch it, because by then the signals look genuinely independent.
What this gets wrong
- Each new channel makes confirmation easier. Adding sources without raising
min_independent_sourcesinflates confidence over time. If you go past five or six channels, raise the threshold. - Survey text is shaped by the question. An NPS follow-up asking "what could we improve" returns complaints because you asked for complaints. That is not spontaneous signal.
- Support tickets over-represent your most engaged users and under-represent the ones who quietly gave up.
- Lost-deal reason fields are written by reps, not buyers. They are your team's theory of why
the deal died. Mark them
fidelity: summarizedand weight accordingly. - Community posts have no attribution and often no customer relationship at all. Frequently tier U, which is the correct outcome.