# Voc Source Custom

> Bring any additional customer signal into the pipeline — survey open-text responses, support tickets, CRM email threads, community posts, churn exit interviews, NPS verbatims, app store reviews. One adapter pattern for every source the dedicated skills do not cover. Trigger on /voc-source-custom, "add our survey data", "pull support tickets into VoC", "connect a new signal source".

- Skill: `aatirs-vault/voc-source-custom` (Agent Skill)
- Install (CLI): `npx skillmds@latest add aatirs-vault/voc-source-custom`
- Raw SKILL.md: https://api.skillmd.com/api/skills/aatirs-vault/voc-source-custom/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra
- Author: Aatirs-Vault (https://skillmd.com/u/aatirs-vault)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/aatirs-vault/voc-source-custom

---


# /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.

```json
{
  "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:

```yaml
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_sources` inflates 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: summarized` and weight accordingly.
- **Community posts have no attribution and often no customer relationship at all.** Frequently
  tier U, which is the correct outcome.

