diff --git a/package-index.json b/package-index.json index ddfb614..ff5216e 100644 --- a/package-index.json +++ b/package-index.json @@ -1,5 +1,24 @@ { "schemaVersion": 1, - "indexRevision": "initial-empty", - "packages": [] + "indexRevision": "2026-08-04-01", + "packages": [ + { + "id": "voc-onboarding", + "name": "Voice of Customer Onboarding", + "version": "1", + "description": "INSTALLER: the moment this package installs, read /agent/brain/voc-onboarding/post-install.md and follow it - it announces the install and states the trigger phrase; nothing else runs at install time. The Voice of Customer onboarding package. Manual install only (installPolicy: manual): an agent installs it with one explicit package install call of its GitHub source. Stages this README and post-install.md into /agent/brain/voc-onboarding/, plus the voc-data-pull, voc-audit, and voc-onboarding-walkthrough skills into the skills root. The package is fully human-triggered: a person saying 'run voice of customer onboarding' (or any ask to begin, resume, or check on VoC onboarding) invokes the voc-onboarding-walkthrough skill, which resolves the workspace from the conversation's own Motion context, finds every reachable VoC platform (OAuth, stored key, or Motion native - a connected Meta workspace counts, since ad comments are customer voice), pins a human-confirmed platform account per workspace, creates one daily voc-sync-- routine per reachable platform with the first run kicked (files land under /agent/brain//data-sources/voc//), and, once data lands, presents the per-integration Voice of Customer summary and offers the Voice of Customer Audit by previewing its plan. The first complete backfill also offers the audit once, asynchronously. The audit runs only on a person's yes or explicit request and saves one compiled page at /agent/brain//data-sources/voc/voice-of-customer-audit.md. Outside those human-triggered runs, nothing self-runs - this package merges no guards and never touches /agent/user.md. One workspace at a time; everything it produces is scoped to that workspace's own folder, so a second workspace in the same org onboards additively without touching the first.", + "categories": [], + "packageManagerVersion": 1, + "installPolicy": "manual", + "updatePolicy": "auto", + "uninstallPolicy": "allowed", + "source": { + "type": "github", + "owner": "Motion-Creative", + "repo": "runneth-apps", + "path": "voc-onboarding", + "ref": "main" + } + } + ] } diff --git a/voc-onboarding/CHANGELOG.md b/voc-onboarding/CHANGELOG.md new file mode 100644 index 0000000..3b4f324 --- /dev/null +++ b/voc-onboarding/CHANGELOG.md @@ -0,0 +1,25 @@ +# Changelog + +## v1 (August 2026) + +Initial release: the Voice of Customer half of `meta-and-voc-onboarding`, extracted as-is +at parent commit `bce9619` and isolated from the Meta onboarding work. + +- **Fully human-triggered lifecycle.** Install stages files and announces the trigger + phrase — nothing else runs at install time. "Run voice of customer onboarding" invokes + the new `voc-onboarding-walkthrough` skill, which owns the whole sequence: workspace + resolution from the conversation's own Motion context, VoC reachability probes (OAuth + + stored keys + Meta connection), human-confirmed account pinning, one daily + `voc-sync--` routine per reachable platform with the first run + kicked, then — once data lands — the per-integration Voice of Customer summary and the + explained audit offer (method preview, additions and reference docs invited). +- **No guards, no roster, no `/agent/user.md` writes.** Because nothing self-fires, the + package needs no activation gate: idempotency comes from routine existence and files on + disk, per the voc-data-pull skill's resume rules. +- **`voc-data-pull` and `voc-audit` copied verbatim** from the parent snapshot, including + the Meta ad-comments integration (one file per creative under `voc/meta-ad-comments/`, + `motion meta creative-comments`), the audit's numbered standalone findings with verbatim + attributed quotes inline and explicit no-signal lines, and the reference-docs intake. + One substitution in `voc-data-pull/SKILL.md`: the setup trigger names this package's + walkthrough instead of the parent's install. The full substitutions list lives in the + README's "Provenance and divergence" section. diff --git a/voc-onboarding/README.md b/voc-onboarding/README.md new file mode 100644 index 0000000..1b5b276 --- /dev/null +++ b/voc-onboarding/README.md @@ -0,0 +1,108 @@ +# Voice of Customer Onboarding + +The VoC-only onboarding package: it finds a workspace's reachable voice-of-customer +integrations, syncs their raw data into the workspace's brain folder on a daily routine, +and turns that corpus into durable creative-strategy insight through the human-gated Voice +of Customer Audit. It is the Voice of Customer half of the `meta-and-voc-onboarding` +package, extracted as-is and isolated from the Meta onboarding work (Account Context +Brain, Creative Attributes, Meta Validation, Knoweth organize) — with one structural +difference: nothing runs at install time. A person triggers everything. + +**Meta ad comments are a VoC integration.** A connected Meta workspace always gets a +`voc-sync--meta-ad-comments` routine (pulled with +`motion meta creative-comments`, one file per creative) alongside the review, support, and +community platforms. That is the only Meta touchpoint in this package. + +## The three parts + +- **VoC Onboarding Walkthrough** (`voc-onboarding-walkthrough/`) — the entry point. A + person says "run voice of customer onboarding" (or asks to begin, resume, or check on + it) and the skill runs its setup phase: workspace resolution from the conversation's own + Motion context, reachability probes across every VoC platform (OAuth connections and + stored keys), human-confirmed account pinning, one daily `voc-sync--` + routine per reachable platform with the first run kicked. Once data lands, its + presentation phase gives the per-integration Voice of Customer summary ("Judge.me: 1,240 + reviews across 6 products, May 2025 – July 2026") and offers the audit by previewing its + plan — split by product, score 1–5, the five buckets, personas — and inviting additions + and reference docs. Re-invoking is always safe: existing routines are skipped, a + mid-backfill invocation reports counts so far, an existing audit gets a rerun offer. +- **VoC Data Pull** (`voc-data-pull/`) — the sync mechanics: platform recipes, account + pinning rules, routine templates, coverage contract (trailing 12 months, bounded pulls), + and the standardized one-file-per-item storage format (one file per creative for Meta ad + comments). Files land under `/agent/brain//data-sources/voc//`. + The first fully covered backfill sends one asynchronous audit offer to the setup + conversation, previewing the audit's plan. +- **Voice of Customer Audit** (`voc-audit/`) — manual skill: separates every synced entry + by product, scores 1–5 for usefulness, and extracts five insight buckets per product + (pain points, trigger moments, objections before purchasing, transformations, standout + language), plus evidence-backed personas for products with 200+ entries. Buckets 1–4 are + numbered lists of distinct standalone findings with verbatim attributed quotes inline + (name, rating, source file) and explicit no-signal lines for empty buckets. Saves one + canonical compiled page at + `/agent/brain//data-sources/voc/voice-of-customer-audit.md`. Runs only on a + person's yes to an offer or an explicit request; requires at least 200 total entries and + a completed backfill. + +## File structure + +``` +voc-onboarding/ + package.json # manifest + README.md # this file -> /agent/brain/voc-onboarding/ + post-install.md # install announce -> /agent/brain/voc-onboarding/ + voc-onboarding-walkthrough/SKILL.md # -> /agent/.agents/skills/voc-onboarding-walkthrough/ + voc-data-pull/ # -> /agent/.agents/skills/voc-data-pull/ + SKILL.md + references/platform-recipes.md + templates/{review,support-conversation,community-post,ad-comments-creative}.md + voc-audit/SKILL.md # -> /agent/.agents/skills/voc-audit/ +``` + +Everything the package produces is workspace-scoped under `/agent/brain//` +(name slugged from the conversation's `Default workspace:` line): raw items under +`data-sources/voc//`, the compiled audit beside them. A second workspace in the +same org onboards additively; folders and routines never merge. + +## Install and run order + +1. **Install (manual, quiet).** An agent installs the package with one explicit install + call. Post-install announces the install and the trigger phrase — nothing else runs. + No guards are merged; the package never touches `/agent/user.md`. +2. **A person triggers onboarding.** "Run voice of customer onboarding" invokes the + walkthrough skill: reachability, account pinning (human-confirmed; auto-pin only for + single-workspace orgs), routines created and kicked. The 12-month backfills run in the + background — data is never pulled inside the conversation. +3. **Data lands; the summary and offer follow.** The first fully covered backfill sends + one asynchronous audit offer; invoking the walkthrough again presents the + per-integration summary and the same explained offer on demand. Both log + `voc-audit-offer` in the workspace changelog so nobody is offered twice by default. +4. **The audit runs on a yes.** The `voc-audit` skill compiles the corpus into the + canonical audit page, honoring any requested additions and supplied reference docs. + Reruns are also manual and regenerate the same page — never dated duplicates. + +## Provenance and divergence + +Extracted from `meta-and-voc-onboarding` at commit `bce9619` (August 2026). `voc-audit/` +and `voc-data-pull/` are byte-for-byte copies of that snapshot except for the +substitutions below. The two packages do not track each other automatically: a fix landing +in either package's copy of a shared file must be mirrored by hand. + +Substitutions (the complete list — everything else is verbatim): + +1. `voc-data-pull/SKILL.md`, "When to use," first bullet: the setup trigger names this + package's walkthrough skill instead of the `meta-and-voc-onboarding` install, and adds + "never at install" to the never-unprompted rule. +2. The walkthrough skill is new (`voc-onboarding-walkthrough`, its own name so it can + never collide with the parent's `onboarding-walkthrough` at the skills root). Its setup + phase carries the parent's post-install steps 0–2 (workspace readout, reachability, VoC + sync setup) with install-turn phrasing adapted to invocation phrasing; its presentation + phase carries the parent walkthrough's VoC summary and audit-offer beats with the + Meta-sequencing preamble (account-context questions, Field 10) removed. +3. `post-install.md` and `package.json` are new, written for the trigger-phrase model: + staged docs live under `/agent/brain/voc-onboarding/` instead of + `/agent/brain/meta-and-voc-onboarding/`. + +Deliberately absent, relative to the parent: all four guards (nothing here self-fires, so +nothing needs `/agent/user.md`), the onboarded-workspace roster, the Account Context +Brain, Creative Attributes, Meta Validation, Knoweth organize, the +`meta-ad-performance-analysis` skill, and the Meta CLI/cache references. diff --git a/voc-onboarding/package.json b/voc-onboarding/package.json new file mode 100644 index 0000000..778b046 --- /dev/null +++ b/voc-onboarding/package.json @@ -0,0 +1,62 @@ +{ + "schemaVersion": 1, + "id": "voc-onboarding", + "name": "Voice of Customer Onboarding", + "version": "1", + "description": "INSTALLER: the moment this package installs, read /agent/brain/voc-onboarding/post-install.md and follow it - it announces the install and states the trigger phrase; nothing else runs at install time. The Voice of Customer onboarding package. Manual install only (installPolicy: manual): an agent installs it with one explicit package install call of its GitHub source. Stages this README and post-install.md into /agent/brain/voc-onboarding/, plus the voc-data-pull, voc-audit, and voc-onboarding-walkthrough skills into the skills root. The package is fully human-triggered: a person saying 'run voice of customer onboarding' (or any ask to begin, resume, or check on VoC onboarding) invokes the voc-onboarding-walkthrough skill, which resolves the workspace from the conversation's own Motion context, finds every reachable VoC platform (OAuth, stored key, or Motion native - a connected Meta workspace counts, since ad comments are customer voice), pins a human-confirmed platform account per workspace, creates one daily voc-sync-- routine per reachable platform with the first run kicked (files land under /agent/brain//data-sources/voc//), and, once data lands, presents the per-integration Voice of Customer summary and offers the Voice of Customer Audit by previewing its plan. The first complete backfill also offers the audit once, asynchronously. The audit runs only on a person's yes or explicit request and saves one compiled page at /agent/brain//data-sources/voc/voice-of-customer-audit.md. Outside those human-triggered runs, nothing self-runs - this package merges no guards and never touches /agent/user.md. One workspace at a time; everything it produces is scoped to that workspace's own folder, so a second workspace in the same org onboards additively without touching the first.", + "installPolicy": "manual", + "updatePolicy": "auto", + "uninstallPolicy": "allowed", + "resources": [ + { + "id": "readme", + "type": "file", + "sourcePath": "README.md", + "executable": false, + "target": { + "root": "agent_brain", + "path": "voc-onboarding/README.md" + } + }, + { + "id": "post-install", + "type": "file", + "sourcePath": "post-install.md", + "executable": false, + "target": { + "root": "agent_brain", + "path": "voc-onboarding/post-install.md" + } + }, + { + "id": "voc-data-pull-skill", + "type": "directory", + "sourcePath": "voc-data-pull", + "executablePaths": [], + "target": { + "root": "agent_skills", + "path": "voc-data-pull" + } + }, + { + "id": "voc-audit-skill", + "type": "directory", + "sourcePath": "voc-audit", + "executablePaths": [], + "target": { + "root": "agent_skills", + "path": "voc-audit" + } + }, + { + "id": "voc-onboarding-walkthrough-skill", + "type": "file", + "sourcePath": "voc-onboarding-walkthrough/SKILL.md", + "executable": false, + "target": { + "root": "agent_skills", + "path": "voc-onboarding-walkthrough/SKILL.md" + } + } + ] +} diff --git a/voc-onboarding/post-install.md b/voc-onboarding/post-install.md new file mode 100644 index 0000000..202dfb8 --- /dev/null +++ b/voc-onboarding/post-install.md @@ -0,0 +1,30 @@ +# Post-install: announce and wait + +Installing the voc-onboarding package stages files and does nothing else. There is no +install-time sequence: no reachability checks, no routines, no pulls, no writes to +`/agent/user.md`, no workspace resolution. Everything runs later, when a person asks. + +When this package's files land, close the installing turn with exactly this: + +> voc-onboarding - installed +> +> Say "run voice of customer onboarding" whenever you're ready and I'll find your +> voice-of-customer integrations, set up the syncs, and take it from there. + +Nothing follows that line. The trigger phrase (or any ask to begin, resume, or check on +Voice of Customer onboarding) invokes the `voc-onboarding-walkthrough` skill - that skill, +and only that skill, owns the sequence: workspace resolution, reachability, account +pinning, sync routine creation, the Voice of Customer summary, and the audit offer. The +audit itself (`voc-audit` skill) runs only on a person's yes to that offer or an explicit +request, and only after its data gate is met. + +What lands where, once the walkthrough runs: + +- Raw VoC items: `/agent/brain//data-sources/voc//` (one file per + item; Meta ad comments one file per creative under `voc/meta-ad-comments/`). +- The compiled audit (later, human-gated): + `/agent/brain//data-sources/voc/voice-of-customer-audit.md`. + +The README's "Install and run order" describes this same lifecycle for humans; this file, +`post-install.md`, is the executable version. If the two ever disagree, fix them together - +do not improvise. diff --git a/voc-onboarding/voc-audit/SKILL.md b/voc-onboarding/voc-audit/SKILL.md new file mode 100644 index 0000000..c033c75 --- /dev/null +++ b/voc-onboarding/voc-audit/SKILL.md @@ -0,0 +1,330 @@ +--- +name: voc-audit +description: Run a manual Voice of Customer Audit on customer-language data already synced into the brain. Use when someone accepts the post-sync audit offer or asks for a VoC audit, review audit, customer audit, analysis of reviews, comments, support messages, pain points, objections, trigger moments, transformations, personas, standout language, or what customers are saying. Never auto-run merely because a source connects. Requires at least 200 total VoC entries and organizes findings by product or service. +--- + +# Voice of Customer Audit + +Mine customer reviews, ad comments, support messages, surveys, community posts, and other +customer language for the real pain, buying moments, objections, transformations, personas, +and phrases that should inform creative strategy and ad copy. + +**The goal is not to summarize customer feedback. The goal is to find the raw material for +ads and make it durable enough to inform validation and future questions.** + +## Run conditions + +This skill is always manual: + +- Run when a person explicitly asks for a Voice of Customer Audit (including the older name, + "review audit"), or answers yes to the package's post-sync or onboarding audit offer. +- Do not run merely because a VoC source connects, a sync routine starts, or new files arrive. +- If an affirmative answer arrives before the initial backfill is complete, say the audit + will be ready when coverage completes. Do not poll or run against a partial backfill. +- A rerun is also manual. New daily VoC files do not silently regenerate the audit. +- The package's offers preview this skill's method (split by product, score 1–5, the five + buckets, personas) and invite additions and reference docs. Honor both: a requested + addition becomes part of this run's output, and supplied reference docs (existing + personas, positioning docs, brand guidelines) are read before analyzing — map evidence + onto existing persona names where they genuinely fit, note where the evidence diverges, + and never force findings into a reference frame the data does not support. + +## What counts as VoC + +- **Customer reviews** are generally post-purchase and may contain pains, objections, + buying triggers, and transformations. +- **Ad comments** may be pre- or post-purchase. Pre-purchase comments are strongest for + pains and objections; do not manufacture triggers or transformations from them. +- **Customer support messages** are post-purchase but negatively selected. They carry rich + friction signal and thin transformation signal; never treat their negative skew as a + verdict on the product. +- **Other customer voice** includes survey responses, DMs, community posts, and sales-call + notes when they are present in the synced VoC data. + +The same method applies across sources, but the audit must name the source mix and account +for each source's bias. + +## Step 0 — Locate the synced data before asking anything + +The audit is scoped to one Motion workspace. Resolve the workspace folder first: +`/agent/brain//`, where `` is this conversation's workspace name +slugged - lowercase, every run of characters that is not a-z or 0-9 becomes one hyphen, +trim leading and trailing hyphens ("Bramblewick NYC" -> `bramblewick-nyc`, "St. Fig & Co." -> `st-fig-co`). + +The package's canonical source is: + +`/agent/brain//data-sources/voc//` + +Start at `/agent/brain//data-sources/voc/` and inspect all platform folders. Do +not ask the person to paste data or choose an integration before checking what is already +synced. + +- Use all available VoC sources unless the person requests a particular platform, product, + or source type. +- Ignore compiled files such as + `/agent/brain//data-sources/voc/voice-of-customer-audit.md`; the audit's + evidence set is the id-keyed raw items inside platform folders. +- Never read another workspace's folder to fill a gap in this one. If this workspace's + `data-sources/voc/` root is absent, its sync has not landed yet - say what paths were + checked instead of borrowing another workspace's data. If the person explicitly wants a + different workspace audited, that audit runs from a conversation in that workspace. +- Only ask for an upload when no matching VoC data exists for this workspace. Say what + paths were checked and why they came up short. + +Record the platform folders, source types, coverage dates, and file counts used. The saved +audit must cite the raw paths it analyzed. + +## Step 1 — Count the entries (hard gate) + +Count discrete VoC entries across the selected evidence set before analyzing. For +`meta-ad-comments`, each comment inside a creative file is one entry (sum the files' +`comment_count`), not one entry per file. + +**If fewer than 200 total entries are available, do not run the audit.** Report the count, +explain that 200 entries are required for reliable cross-entry patterns, and stop without a +partial audit. + +At 200 or more, record the total and proceed. + +## Step 2 — Group by product or service + +Identify the products or services represented before extracting patterns. Every subsequent +step runs separately per product or service. + +- Use the raw record's product reference and the content itself. +- Record the entry count per product. +- If attribution is ambiguous and materially changes the result, ask one focused + clarification before proceeding. +- If all entries concern one offering, skip grouping. + +The 200-entry audit gate is across the selected evidence set. The persona gate in Step 6 is +per product. + +## Step 3 — Score entry quality (1–5) + +Score every entry for usefulness: + +| Score | Meaning | +|---|---| +| **1** | Garbage or no signal: gibberish, emoji-only, or generic fragments such as "great product." | +| **2** | Low signal: short and vague, with little specific detail or emotion. | +| **3** | Moderate: some product or experience detail, but limited emotional depth. | +| **4** | High quality: specific experience, before/after detail, or a meaningful feeling. | +| **5** | Gold: vivid, emotional, detailed customer language with strong creative value. | + +Score-5 entries are the priority. For roughly 2,000 or more entries, score +programmatically or in batches rather than one at a time. If sampling is necessary, state +that plainly and record the sample size; never present a sample as an exhaustive read. + +## Step 4 — Filter + +- Discard score-1 entries. +- Analyze scores 2–5, emphasizing scores 4–5. +- Scores 2–3 may confirm patterns but should not supply featured quotes. +- Never manufacture a theme when too little quality evidence supports it. + +## Step 5 — Extract the five insight buckets + +Run all five buckets separately for each product or service. In Buckets 1–4, present each +bucket as a **numbered list of distinct findings** — Product → bucket → 1, 2, 3 — never one +flowing paragraph. Each numbered point is a standalone finding stated in one or two clear +sentences, not a blended narrative; a reader must be able to scan the list and take any +point on its own. When a finding is supported by a quote, the quote is unambiguously +verbatim and attributed inline — exact customer wording with the reviewer's name, star +rating where the source has one, and the raw source file — never paraphrased or folded into +prose where it is unclear who said what. A quote is not required on every point; the list +must be scannable and the quotes that do appear must be clean and attributable. A bucket +with no real finding gets one explicit line saying so ("No strong signal in this bucket for +this product") — never a manufactured entry to fill the template. + +### Bucket 1 — Pain points + +Ask: **What problem were customers experiencing before they found the product?** + +Look for duration, prior failed solutions, daily-life effects, and the emotional weight of +the problem. Each theme needs enough repeated evidence to be a pattern. + +### Bucket 2 — Trigger moments + +Ask: **What finally made them buy?** + +Look for the event or realization that turned interest into action: a life event, +recommendation, diagnosis, deadline, breaking point, or exhaustion with alternatives. +Do not infer trigger moments from source types that cannot support them. + +### Bucket 3 — Objections before purchasing + +Ask: **What nearly stopped them from buying?** + +Look for product-level objections an ad can answer: price, skepticism, fear after failed +alternatives, fit, size, ingredients, or uncertainty about results. + +Do not turn operational friction outside the product story—shipping delays, courier +problems, service wait times, website issues—into an ad objection. When those patterns are +loud, include one operational flag outside the creative-strategy buckets. + +### Bucket 4 — Transformations + +Ask: **What changed after they used the product?** + +Look for concrete outcomes, emotional shifts, and the customer's own before/after language. +Do not claim transformations from pre-purchase sources. + +### Bucket 5 — Standout language and ad-ready phrases + +Collect the strongest exact quotes from all buckets in one swipe file. The inclusion test is +whether the language is funny, visceral, candid, surprising, memorable, or unusually +specific—not whether it merely praises the product. + +Never rewrite a quote. + +- For reviews: `"[Quote]" — [Name or identifier], [X]★` +- For comments or support messages: `"[Quote]" — [Name or handle]` +- Include the product and raw source path or item identifier. +- Briefly state why the phrase stands out. + +Flat praise such as "great product" or "highly recommend" does not qualify. + +## Step 6 — Build customer personas + +Only build personas for a product or service represented by at least 200 entries. Products +below that threshold still receive all five buckets but skip personas. + +Identify three to five evidence-backed personas per qualifying product. Never pad the count. +For each persona include: + +- **Persona name** — a short, evocative label. +- **Who they are** — life context and identity signals visible in the evidence. +- **Pain** — primary pain from Bucket 1. +- **Trigger** — buying moment from Bucket 2. +- **Objection** — what nearly stopped them from Bucket 3. +- **Transformation** — what changed from Bucket 4. +- **How they talk** — characteristic vocabulary and tone. +- **Estimated share** — a rough proportion of that product's entries, clearly labeled as an + estimate. + +## Step 7 — Save the compiled audit + +The chat output is useful for the current conversation; the compiled brain page is what +makes the insights available to validation and future questions. + +Write or replace this single canonical file: + +`/agent/brain//data-sources/voc/voice-of-customer-audit.md` + +This file is compiled understanding, not raw evidence. Never edit the source item files. +The saved page uses the same structure as the chat output — numbered standalone findings +per bucket with verbatim attributed quotes inline and explicit no-signal lines — not a +paragraph-summary rewrite; the readable structure applies everywhere the audit lands, not +just the conversation view. Use the Knoweth compiled-page contract: + +```yaml +--- +page_type: compiled +substance: interpretation +sources: + - /agent/brain//data-sources/voc// +last_compiled: +confidence: +tags: + - voice-of-customer + - customer-language + - creative-strategy + - pain-points + - objections + - trigger-moments + - transformations + - personas +--- +``` + +List the evidence scope near the top: source platforms and types, coverage dates, total +entries, analyzed entries, discarded score-1 entries, whether sampling was used, and counts +per product. Cite representative raw files for each claim and every quote. A directory alone +is not enough provenance. + +On rerun, regenerate this canonical page from the current evidence set. Do not append another +audit or create dated duplicates. Update `/agent/INDEX.md` with one entry that names the +workspace (the index is org-wide) and aliases: `Voice of Customer Audit`, `VoC audit`, +`review audit`, `customer insights`, `pain points`, `objections`, `trigger moments`, +`transformations`, `personas`, and `customer language`, each prefixed or suffixed with the +workspace name. Append a dated `voc-audit-completed` entry to +`/agent/brain//_changelog.md` with the evidence coverage and canonical audit path. + +## Output format + +Produce a separate section for each product or service: + +```text +VOICE OF CUSTOMER AUDIT + +Evidence: [platforms and source types] +Coverage: [date range] +Entries received: [X] | Analyzed: [X] | Discarded (score 1): [X] + +PRODUCT / SERVICE: [Name] + +PAIN POINTS +1. [Standalone finding, one or two sentences] + "[Exact quote]" — [Name], [X]★ · [source file] +2. [Standalone finding — a quote is optional, not required per point] +3. [Standalone finding] + +TRIGGER MOMENTS +1. [Standalone finding] + "[Exact quote]" — [Name], [X]★ · [source file] +2. [Standalone finding] + +OBJECTIONS BEFORE PURCHASING +1. [Standalone finding] +2. [Standalone finding] + +TRANSFORMATIONS +No strong signal in this bucket for this product. +[Use this explicit line when a bucket has no real finding — never a manufactured entry] + +STANDOUT LANGUAGE AND AD-READY PHRASES +"[Exact quote]" — [attribution] · [why it stands out] · [source] + +PERSONAS +[Persona name] — approximately [X]% of this product's entries +Who: [...] +Pain: [...] +Trigger: [...] +Objection: [...] +Transformation: [...] +Voice: [...] + +OPERATIONAL FLAGS +[Only when loud non-creative friction exists] +``` + +Quotes may support individual findings inline, always verbatim and attributed; the +standout-language swipe file still collects the strongest lines in one place regardless of +where they also appear. + +## How downstream work uses the audit + +- Pain points inform the pain/desire anchor in creative strategy. +- Trigger moments supply hook material. +- Addressable objections inform objection-handling ads. +- Transformations supply aspirational and social-proof language. +- Standout phrases provide customer-native hook and copy language. +- Personas inform audience, angle, and message decisions. + +When validation or a future answer asks why customers respond, what to make next, which +message to test, or what customers love, object to, or misunderstand, read +`voice-of-customer-audit.md` first, then inspect the cited raw evidence when the claim needs +verification or greater detail. The audit informs customer-side WHY; it never substitutes +for live performance metrics or the creative content layer. + +## Quality rules + +- Clearly separate repeated evidence from inference. +- Treat source bias explicitly. +- Never present sampled analysis as exhaustive. +- Never invent a persona, finding, or theme to fill a template — an empty bucket says so + explicitly instead. +- Every numbered point stands alone; no blended multi-finding narratives. +- Every quoted phrase remains verbatim and attributable. +- Every durable claim in the saved audit traces to raw VoC evidence. diff --git a/voc-onboarding/voc-data-pull/SKILL.md b/voc-onboarding/voc-data-pull/SKILL.md new file mode 100644 index 0000000..8fdbdc9 --- /dev/null +++ b/voc-onboarding/voc-data-pull/SKILL.md @@ -0,0 +1,475 @@ +--- +name: voc-data-pull +description: | + Pull raw voice-of-customer data - product reviews, support conversations, surveys, + community posts, and comments - from an available VoC platform into standardized files in + the current workspace's brain folder, one file per item (Meta ad comments: one file per + creative). Use when ANY VoC platform - one with a recipe + (Judge.me, Trustpilot, Yotpo, Junip, Okendo, Stamped, Reviews.io, Gorgias, Intercom, + Zendesk, Klaviyo, Attentive, Gong, Hotjar, Reddit, Discord, YouTube) or any other + reachable platform whose data is customer voice - is reachable by any path - OAuth + connection, stored API key, or Motion native - and its data should land in files, or when + the user asks to "pull the reviews", "dump the reviews", "pull support tickets", "sync + customer conversations to files", or "run the VoC data pull". + Do NOT use for analyzing customer voice (use the voc-audit skill), building integration + guides, or one-off API questions about a platform. +--- + +# VoC Data Pull + +Pull raw voice-of-customer (VoC) items from a connected platform and write them into the +current workspace's brain folder as standardized files: **one file per review, support +ticket/conversation, or community post - and for Meta ad comments, one file per creative +carrying every comment on that creative** - each with a metadata header and the content body. +Creative strategy packages build on these files, so shape consistency matters more than volume. + +## When to use + +- The voc-onboarding-walkthrough skill (the voc-onboarding package's entry point, fired by + a person asking to run their Voice of Customer onboarding) is executing its setup phase - + that ask is the trigger - or the team or user asks to set up the VoC data sync -> run + the "Set up the recurring sync" procedure below. Setup never happens at any other + unprompted moment - never at install, never just because a platform connects. +- A `voc-sync--` routine run is executing (the normal path - see Recurring sync + runs below). +- The user asks to pull, refresh, or extend VoC data, or asks for reviews/support + conversations "in files" or "in the brain". Route these through the routine, not an + in-conversation pull: make sure `voc-sync--` exists (setup procedure below), + then `routine run --id ` for an immediate refresh. + +Run one platform per pull unless asked otherwise. Never wait for confirmation to start - +the window rules below fully determine what to pull. + +## Hard boundaries + +- **Read-only against platforms.** List/read endpoints only. Never write, reply, or delete + through a VoC platform API. +- **Bounded pulls, complete within the bound.** Default to the trailing 12 months; the date + window is the coverage contract - everything inside it gets pulled, nothing outside it + does. Use a server-side date bound where the platform's recipe names one (e.g. Yotpo's + `since_date`, Klaviyo's `created` filter, Gong's `fromDateTime`, Intercom's + `POST /conversations/search`); everywhere else, page newest-first and stop once items are + older than the cutoff. The 50-pages-per-run cap is runaway protection only: hitting it + means "pause, report, and continue in further batches until the window is covered" - + never "done." A run that ends short of coverage states "coverage stopped at + " in its summary (Step 4), and the next run resumes + from that date before the normal incremental window. This section owns the coverage + contract; recipes only state per-platform mechanics. +- **Raw data files are separate from integration guides.** Never write pulled data into + `/agent/brain/integrations//` - the integration guide spec explicitly forbids raw + dumps in guides. VoC data lives only under `/agent/brain//data-sources/voc/`. +- **The later audit is not a pull output.** The Voice of Customer audit skill runs later in + onboarding, after raw data has landed. It writes + `/agent/brain//data-sources/voc/voice-of-customer-audit.md`. This raw-pull skill + never creates or updates that file, and its absence during install or backfill is expected. +- **PII: leave `author_contact` null.** The unified template keeps the field, but the policy + call on storing customer emails is pending. Do not populate it until told the policy allows + it. Raw platform payloads are NOT persisted in output files (see the file format below), so + do not paste payload JSON - which carries reviewer emails and other PII - into any brain + file. + +## Step 1 - Resolve the platform and connection path + +Two connection paths exist and the pull mechanics differ: + +| Path | Platforms | How to call the API | +|---|---|---| +| Pipedream OAuth | `judge_me`, `trustpilot`, `yotpo`, `gorgias_oauth`, `intercom`, `reddit`, `zendesk`, `klaviyo`, `attentive`, `gong`, `hotjar`, `discord`, `youtube_data`, `junip` and `reviews_io` (keys-auth in Pipedream) | `integrations` CLI: check `integrations status --app `, then `integrations proxy --app --account --method GET --path ` (or the registered app command). The account id comes from the workspace's pin (see "Pin the account" under setup), never from picking off the list at pull time. | +| Stored secret (customer API key) | `okendo`, `stamped` - and **any platform above whose org stores a key instead of connecting OAuth** | `secure-fetch` (`secure-fetch run --url --secret-key ...`) per `/runneth/references/secure-fetch-cli--command-contracts.md`. If no stored key exists, request one via the secret-collection flow - never ask for the key in chat. | +| Motion native | Meta ad comments | `motion meta creative-comments` (no Runneth connect involved) | + +The path is how this customer set the platform up, not a property of the platform: any +VoC platform may arrive as an OAuth connection **or** a stored secret, so an +availability check always checks both `integrations status --app ` and the stored +secrets - for +every VoC platform, not just Okendo/Stamped (which are secrets-only because no +Pipedream app exists for them). The secret store cannot be listed (the runtime refuses +Bash reads of `/agent/.runtime/secrets`, and neither `secret` nor `secure-fetch` has a +list command): probe per platform by attempting its documented secret key +(`secret run --env KEY= -- true`, or a bounded `secure-fetch run`) and treat +a "secret not available" error as not stored. + +**Accounts are org-level; ownership is per workspace.** Connections live on the org's VM +with no workspace tag, and one org can hold several accounts of the same platform - or one +account genuinely shared by every workspace. Which account belongs to this workspace is +decided once, by a human, at setup ("Pin the account" below), and every pull afterwards +addresses that exact account (`--account `). Count is not evidence: a +single connected account may belong to a different workspace, so reachability alone never +decides ownership. + +**Driving a platform from a stored secret (instead of the proxy):** the recipe's endpoints +and mappings stay the same, but you must supply what the proxy normally injects. Get the +base URL, auth header shape, and platform notes from the app's registry-backed guide (the +`integrations` CLI catalog / the registered app's guide - it exists even before a connect). +Known proxy-injected specifics: Klaviyo's `revision` header is auto-sent only through the +proxy - on the secrets path send it yourself (see the recipe); Gong, Gorgias, and Zendesk +use per-account hosts - get the account's host from the customer, never guess it. A +platform whose API only accepts OAuth tokens cannot be driven by a static key at all - the +bounded verification call settles that per org; if it fails on the secrets path, report the +gap, do not improvise auth. + +Exact endpoints, pagination, discovery steps, and field mappings for every platform are in +`references/platform-recipes.md` in this skill folder. Read the recipe for the target +platform before calling anything. + +**The recipe list is not the scope - VoC data is the scope.** Any other reachable platform +whose data is customer voice (reviews, support conversations, surveys, community posts, +comments) is in scope with no recipe at all: resolve its connection path exactly as above +(OAuth via the `integrations` proxy, or a stored key via `secure-fetch` with the registry +guide's base URL and auth shape), then pull it through Step 2's no-recipe path. Platforms +whose data is not customer voice (commerce, analytics, ads infra, internal comms) stay out +of scope regardless of reachability. Priority when both kinds are reachable: handle the +recipe'd platforms first, then the no-recipe ones - and a platform that has a recipe is +always pulled through its recipe, never freelanced through the no-recipe path. + +## Step 2 - Pull with the platform recipe + +Follow the recipe first: run its discovery step when it has one (Trustpilot businessUnitId, +Yotpo appKey, Okendo storeId, Stamped storeHash), then page through the list endpoint with +the recipe's pagination style, applying the date bound. + +For support platforms (Gorgias, Intercom, Zendesk), also fetch each conversation's messages +so the file body can carry the full conversation. + +**The data is the mandate; the recipe is guidance.** Many recipes are doc-grounded and the +platform's live API is the truth. When reality differs from the recipe - an endpoint moved, +a field is named differently, pagination works another way - adapt from the live payload and +the platform's docs and **keep pulling**. Never abort or stall a pull because a recipe is +stale or incomplete. Specifically: + +- A recipe field name that does not exist in the real payload -> find the equivalent field + and use it; if there is no equivalent, write `null` and move on. +- An endpoint that 404s -> probe the platform's current API for the listing that returns + the same data; use it. +- Unknown pagination -> discover it from the response shape (cursor, page number, next + link) and page it fully. +- Record every deviation in the run report (what the recipe said, what the API actually + was) so the recipe gets corrected. + +**No recipe at all? Same job, live-first.** A VoC platform with no section in +`references/platform-recipes.md` is pulled the same way a stale recipe is handled - from +the live API, start to finish: find the platform's listing endpoints for its customer-voice +data (its API docs and the registry guide are the sources), discover pagination from the +response shape, apply the date window client-side unless a server-side bound exists, and +map the real payload onto the unified record - `source_type` is whichever of the four +existing types the data actually is (survey responses are `review` when rated, +`support_conversation` when conversational), the platform's registry slug is the folder +and `source_platform`, and anything org-specific rides in `custom`. Pull the raw data you +find. Report it as "no recipe - live-adapted" with the calls used, so a recipe can be +written from the run. + +What never flexes: the hard boundaries (read-only, PII rules, bounded windows), the output +contract (unified record fields, file shape, id-keyed paths), and honest coverage reporting. +The only legitimate reasons to stop are auth that fails, an API the org's plan does not +expose, or a hard boundary - and each of those is reported as an explicit gap, never +silently. A missing recipe is never a reason to stop. + +## Step 3 - Write the files + +### Folder convention + +Root: `/agent/brain//data-sources/voc//`, where `` is the +Motion workspace this pull belongs to, slugged - lowercase, every run of characters that is not a-z or 0-9 becomes one hyphen, trim leading and trailing hyphens ("Bramblewick NYC" -> `bramblewick-nyc`, "St. Fig & Co." -> `st-fig-co`). **Every pull is scoped to one workspace.** The workspace folder is the boundary that +keeps two brands' customer voice apart: a pull for one workspace never writes into another's +folder, and pulls are never merged into a shared root. Items are keyed by `external_id`, so a +shared root would not overwrite - it would silently accumulate two brands' reviews into one +corpus, which is worse. Under the workspace folder, all VoC pulls live under the shared +`voc/` parent, one flat folder per platform, no type subfolders. Use the platform's registry +slug as the folder name (`judge_me`, `gorgias_oauth`, ...; Meta ad comments use +`meta-ad-comments` - it sits at the same level as every other platform folder). +The later Voice of Customer audit is the one non-item artifact at the `voc/` root: +`/agent/brain//data-sources/voc/voice-of-customer-audit.md`. It is created by the +later audit skill, not by these pull or recurring-sync procedures. Raw platform folders remain +items-only. +The filename prefix carries the source type: + +- Reviews: `/agent/brain//data-sources/voc//review-.md` +- Support tickets/conversations: `/agent/brain//data-sources/voc//ticket-.md` +- Meta ad comments: `/agent/brain//data-sources/voc/meta-ad-comments/creative-.md` + - **one file per creative**, carrying every comment pulled for that creative +- Community posts/comments (Reddit): `/agent/brain//data-sources/voc/reddit/post-.md` + and `/agent/brain//data-sources/voc/reddit/comment-.md` + +Every raw-item path is keyed by the item's `external_id` and nothing else - for Meta ad +comments the creative is the item, so the file is keyed by its `creative_asset_id`. This is +the contract that re-pull dedupe, ticket overwrite, and the recurring-sync incremental window +all depend on. Always use the workspace-scoped path above; never adopt a different surrounding +layout or invent additional hierarchy. + +Re-pull write policy, per source type: + +- **Reviews**: immutable at the source - skip files that already exist. +- **Support tickets**: live over time - overwrite the ticket's file when the source shows a + fresher `updated_at` or more messages. +- **Community posts**: content is fixed but engagement mutates (reactions, reply counts, + scores) - overwrite when the item is inside the run's pull window, skip when it is older + than the window. +- **Meta ad comments**: the creative's file is the unit - whenever a run pulls comments for + a creative, regenerate that creative's file whole (full comment set, updated totals and + engagement). Never append to an existing file. + +The id-keyed path is what makes every overwrite land on the same file. + +### File format + +Every file has the same three-part layout, top to bottom (copyable skeletons for every +source type are in `templates/`): + +1. **Headline + human header** - an `# H1` identity line, then a block of bold-label lines + (`**Label:** value`, each line ending with two trailing spaces so markdown keeps the line + breaks). The header is a human-readable *projection* of the metadata for that source + type - humanized values (`No` instead of `false`, `—` for empty), only the fields that + are meaningful for the item, and every key in `custom` surfaced as its own label (that is + where org-specific lines like `Category` / `Customer tier` come from). It is never the + source of truth; the metadata block below is. + - Reviews: `# Review # - "" <stars>` (`★`/`☆` out of 5; omit the + quoted title when null). Labels: Platform, Rating, Reviewer, Date, Product, Verified + buyer. + - Support: `# Ticket #<external_id> - Re: <subject>`. Labels: Platform, Date, Status, + Channel, Customer, each `custom` key, Tags, Messages (count + last activity). + - Meta ad comments (one file per creative): `# Ad comments - creative <creative_asset_id>`. + Labels: Creative asset id, Preview file URL (when the pull returns one), Total + comments, Newest comment, plus every identifying field the pull returns (e.g. ad + name) as its own label. + - Community posts: `# Reddit post — "<title>"` (comments: `# Reddit comment #<id>`). + Labels: Subreddit, Author, Date, Upvotes, Replies, and In reply to for comments. +2. **The content**, between two `---` rules: the review text as plain prose, the **full + conversation** for support items (one `### <author> (<role>) - <timestamp>` section per + message, in order), the comment text for community posts, or **every comment on the + creative** for Meta ad-comment files - one `### <author> - <timestamp>` section per + comment, newest root first, each reply directly under its parent and marked "in reply + to", with the comment's reactions and reply count in the section heading. +3. **The metadata block** - a collapsed section, exactly: + + ``` + <details> + <summary>Metadata (unified VoC record)</summary> + + [fenced yaml block] + + </details> + ``` + + The yaml inside is the machine contract downstream packages parse. It is ONE flat record + shape for every VoC item. **All fields are always present; use `null` when the source + lacks the concept.** Never drop a field and never add org-specific fields at the top + level (org-specific platform fields ride in `custom`). Keep the blank lines around the + fenced block - markdown inside `<details>` needs them. + +Do NOT write the raw platform payload into the file. Map the recipe's fields, keep anything +org-specific in `custom`, and leave the rest of the payload behind. + +### The unified metadata record + +Common fields (every item; Meta ad-comment files use the per-creative record below instead): + +| Field | Meaning | +|---|---| +| `source_platform` | Registry slug (`judge_me`, `gorgias_oauth`, ...) | +| `source_type` | `review` \| `support_conversation` \| `community_post` | +| `external_id` | The platform's id for the item | +| `created_at` | ISO 8601 | +| `title` | Review title / support subject; null when absent | +| `body` | Always populated in the file body section (see below), not duplicated in the metadata block | +| `author_name` | Reviewer/customer/commenter display name | +| `author_contact` | **Always null for now** (PII policy pending) | +| `reply_count` | Number of replies/messages **beyond the root item** (a 4-message ticket has `reply_count: 3`); null when unknown | +| `parent_ref` | For reply items (e.g. Reddit or YouTube comments): the parent item's `external_id`. Null for root items. | +| `source_url` | Link back to the item on the platform. Set it only from the recipe's `source_url` mapping; most platforms provide none in the list payload - then it is null. Never invent a URL pattern. | + +Review fields (null for the other types): + +| Field | Meaning | +|---|---| +| `rating` | 1-5 integer. Intercom CSAT (`conversation_rating`) maps here too. | +| `product_ref` | Platform product reference. Null for Trustpilot (company-level reviews). | +| `verified` | Verified-buyer boolean | + +Support-conversation fields (null for the other types): + +| Field | Meaning | +|---|---| +| `status` | open/closed/resolved <- Gorgias `status`, Intercom `state` | +| `channel` | email/chat/phone/social <- Gorgias `channel`/`via` | +| `tags` | List of tag names <- Gorgias `tags[]` (e.g. `csat_excluded`) | +| `updated_at` | ISO 8601 - tickets live over time | +| `custom` | Pass-through object of platform custom fields <- Gorgias `custom_fields`, Intercom `custom_attributes`. Carry keys as-is; do not enumerate or rename. | + +Community-post fields (null elsewhere): + +| Field | Meaning | +|---|---| +| `reactions_total` | Total reactions on the comment, or upvotes/score for community posts | + +`body` is the file's content section (part 2 of the layout), not a yaml key - it is the one +template field that lives outside the metadata block. + +### The per-creative ad-comments record + +Meta ad-comment files carry a per-creative record instead - the creative is the item +(skeleton in `templates/ad-comments-creative.md`): + +| Field | Meaning | +|---|---| +| `source_platform` | `meta-ad-comments` | +| `source_type` | `ad_comments` | +| `creative_asset_id` | <- the group's `creativeAssetId` | +| `preview_file_url` | <- the group's `previewFileUrl`, the only URL the pull returns (a preview of the creative, not a post permalink); null when absent. Never invent a URL. | +| `comment_count` | Total comments this file carries for the creative | +| `newest_comment_at` / `oldest_comment_at` | ISO 8601 bounds of the comment set | +| `custom` | Pass-through of every other field the payload carries at the group level - `adName` -> `ad_name`, plus the coverage-reported cached total (`coverage.commentsTotal` -> `comments_total_reported`) when it exceeds `comment_count` | +| `comments` | One entry per comment: `id`, `created_at`, `author_name`, `author_contact` (always null - PII), `platform` (facebook/instagram), `reactions_total`, `reply_count`, `parent_ref` (replies arrive nested under their parent in the payload - set this to the parent comment's `id`; null for roots). Comment text lives in the body section, not here. | + +Per-platform field mappings (`rating` <- Judge.me `rating` / Trustpilot `stars` / Yotpo +`score` / Stamped `reviewRating`, and so on) are in the recipes reference - each platform +adapter is a field-mapping exercise, not design work. + +## Set up the recurring sync + +All pulling happens through one daily routine per connected platform (`voc-sync-<workspace>-<platform>`). +**Setup runs when this package finishes installing (the install is the ask) or when asked +directly** - never at any other unprompted moment. When triggered, do this for each +available VoC platform - recipe or no recipe (Step 1's scope rule), and available means +the org can reach it by any path (OAuth connection, stored API key, or Motion native; +Step 1 resolves which): run +`routine list --search "voc-sync-<workspace>-<platform>"` - routine absence is what needs setup, not +folder state: + +- **Routine exists** -> do nothing (already set up). +- **Routine absent** -> pin the account, create the routine, kick its first run, and tell + the user. Exactly this: + +1. **Pin the account.** Resolve which account of the platform belongs to this workspace - + by asking, never by inference: + - OAuth path: `integrations accounts --app <slug>`, then show each account's identity + (account name and account id) and ask. **Even when exactly one account is listed, + ask**: "Is <accountName> the <platform> account for <workspace>? It may be shared + with other workspaces, or this workspace's account may not be connected yet." A lone + account can belong to a different workspace, so its existence is not an answer. The + one exception: when the org has exactly one Motion workspace, everything on this VM + belongs to it - pin a lone account without asking and note the auto-pin in the setup + report. Two or more workspaces, or two or more accounts, always means asking. + - The user names one -> that account id is the pin. Sharing is legitimate: two + workspaces may pin the same account, each keeping its own routine and folder. If the + user says the shared account carries more than this workspace's data (a multi-brand + helpdesk, a multi-store review account), record the slice they name (brand, store, + tag) and apply it as a filter in every pull. + - The user says this workspace's account is not connected yet -> create nothing for + this platform and record it as pending in the setup report (note that the existing + connection belongs to another workspace). Once the account is connected, setup runs + on ask, like any platform connected later. Never pin an account because it is the + only one there. + - Stored-secret path: the key is the account. Confirm ownership the same way, using + the identity the bounded verification call returns (store name, subdomain, account + label). + - Motion native (Meta ad comments): no pin needed - `--workspace-id` already scopes it. + +2. Create the routine. Fill in the real current conversation id for `<conversation-id>`, + the resolved workspace folder name for `<workspace>`, the resolved workspace id for + `<workspaceId>`, and the pinned account's name and id for `<accountName>` / + `<accountId>`; keep the cron and the name shape exactly as written. **Every one of those + values is written out literally, never left as a placeholder for the run to resolve:** + routine runs execute in their own conversation with no workspace attached, so a routine + that says "the current workspace" or "the connected account" has nothing to resolve and + will either fail or guess. The workspace also belongs in the routine name because + routines are VM-wide - a bare `voc-sync-<platform>` collides with another workspace's + routine, and that collision is what mixes two brands' data: + + ``` + routine add --name "voc-sync-<workspace>-<platform>" \ + --delivery "Daily incremental success: no notification - the deliverable is the files under /agent/brain/<workspace>/data-sources/voc/<platform>/. On the first fully covered backfill across any voc-sync-<workspace>-* routine, if /agent/brain/<workspace>/_changelog.md does not already contain a voc-audit-offer entry, send one brief note to web conversation <conversation-id>: name the source that finished, say the customer voice is ready, and offer a Voice of Customer Audit by previewing the plan in your own words - it will separate every entry by product, score each 1-5 for usefulness, and break the strong ones into five buckets (pain points, trigger moments, objections, transformations, standout language) plus personas per qualifying product - then ask whether they'd like anything added or have existing docs (like personas) to use as reference. Then append a dated voc-audit-offer entry to /agent/brain/<workspace>/_changelog.md. Never run the audit without a person's yes. If the run fails, the pinned account is disconnected, or coverage is incomplete, send a brief note to the same conversation with conversation send --to <conversation-id>." \ + --prompt "Run the voc-data-pull skill for <platform> as a recurring sync run for Motion workspace <workspace> (workspace id <workspaceId>). Pull only from the pinned account <accountName> (account id <accountId>): pass --account <accountId> on every integrations proxy call and never use another account of this platform, even if others are connected. Write every file under /agent/brain/<workspace>/data-sources/voc/<platform>/ and nowhere else; pass --workspace-id <workspaceId> on Motion commands that take it. Follow the skill's Recurring sync rules exactly - they define the pull window, disconnect handling, and coverage reporting." \ + --cron "0 6 * * *" + ``` + + When a slice filter was recorded at pin time, append it to the prompt as one literal + sentence (for example "Only pull items for brand <brand>."). On the stored-secret path, + replace the `--account` sentence with the secret key and confirmed identity ("Use the + stored key <SECRET_KEY> for <identity>; if the key stops working, report - do not + substitute another credential."). On the Motion-native path (meta-ad-comments), there is + no pinned account: replace the `--account` sentence with the workspace scope ("Ad comments + come through this workspace's own Meta connection - scope every pull with + `--workspace-id <workspaceId>`; there is no account to pass."). + +3. Kick the first sync run now (it happens in the background; the window rules below make + it a full backfill when no files exist yet, incremental otherwise): + + ``` + routine run --id <routine-id> + ``` + +4. Tell the user in one or two sentences: the initial pull is running in the background and + the data stays updated daily. Explain that once the first full backfill is ready, Runneth + will offer a manual Voice of Customer Audit. Do not run or present the audit now, and do + not mention routine mechanics unless asked. + +**Never run the pull inside the user's conversation.** All pulling happens in the routine's +runs; a one-off refresh beyond the daily cadence is `routine run --id <routine-id>`. + +**Key-auth gate (junip, reviews_io, okendo, stamped, and any key-stored platform):** the +key is per-customer, so before creating its routine verify access with one bounded call +(junip: `GET /v1/stores`; reviews_io: the merchant reviews list; okendo: `GET /v1/stores`; +no-recipe platforms: the smallest listing call the registry guide or API docs name); if it +fails, tell the user the key needs replacing and create nothing for that platform. + +## Recurring sync runs + +When you are executing one of those routine runs, these rules apply on top of the normal +skill flow: + +- **Pull window** (this is what makes runs incremental - id-keyed files only dedupe + writes, they do not shrink API paging): + - `/agent/brain/<workspace>/data-sources/voc/<platform>/` empty -> pull the trailing 12 months (this + run is the backfill). + - Otherwise -> pull from the **newest existing item's `created_at` minus 2 days** + (overlap for safety; self-healing across paused or failed runs), never further back + than 12 months. For support tickets, use the platform's `updated_at` bound where the + recipe has one, so updated conversations are re-pulled and overwritten. For + meta-ad-comments, the newest existing item is the newest `newest_comment_at` across + the creative files. +- **Pull only the pinned account** named in the routine prompt (`--account <accountId>`, + or the named secret key). Other accounts of the same platform - including ones connected + after setup - are never pulled by this routine: a new account belongs to whichever + workspace confirms it in its own setup pass, not to whoever's routine runs first. +- **Pinned account disconnected** (the proxy answers that the account is not connected, or + the stored key stops working) -> pull nothing, say so in the run summary, and send the + failure note. Never fall back to another connected account of the platform. Do not pause + or cancel the routine: reconnecting the same account self-heals (the routine already + exists, the next run resumes). If the account is gone for good, the workspace re-pins + in a new setup pass. +- **Delivery**: daily incremental success is silent - the files are the deliverable and the + run summary is recorded in run history. The first of this workspace's + `voc-sync-<workspace>-*` runs to complete full backfill coverage sends one offer to the + delivery conversation — not a bare yes/no question but a short preview of what the audit + will do (split by product, score 1–5, the five buckets, personas), closing with an + invitation to add anything or supply reference docs such as existing personas. Before + sending, check `/agent/brain/<workspace>/_changelog.md` for a `voc-audit-offer` entry; if + found, stay silent. After sending, append a dated `voc-audit-offer` entry naming the + source whose backfill completed. This is an offer only: never run the audit until a + person says yes. Failures, disconnects, and incomplete coverage get a brief note to the + delivery conversation named in the routine. +- Everything else - boundaries, recipes, file format, coverage reporting - is the normal + skill contract. + +## Step 4 - Report + +After the pull, report: platform, account used, date bound, item count written, folder path, +whether the full date window was covered, and any items skipped. If a run hit the page cap, +report the batches used and confirm coverage continued to the cutoff. If a run ends before +the window is covered, state "coverage stopped at <oldest covered date>" in the +run summary - the next run reads it from routine history and resumes from that date before +applying the normal incremental window. If the platform recipe +was doc-grounded (not live-verified), say which calls you verified live during this pull. + +## Known v1 gaps - state these honestly when relevant + +- **Per-platform evidence and caveats live in one place: each recipe's header and bullets + in `references/platform-recipes.md`** (live-verified vs registry-verified vs + doc-grounded, plan gates, key requirements, org-specific pull targets, API limits). Read + the target platform's recipe before promising data, and state its evidence level and + caveats honestly when relevant. Do not restate them here. +- **Template deviation, pending sign-off**: the proposed unified template lists a `raw` + (untouched payload) column; this package deliberately does not persist raw payloads in + files - leaner files, and no platform PII stored beyond what the mapped fields carry. If + the template sign-off insists on `raw`, the file format gains a collapsed raw-payload + section back. diff --git a/voc-onboarding/voc-data-pull/references/platform-recipes.md b/voc-onboarding/voc-data-pull/references/platform-recipes.md new file mode 100644 index 0000000..9f72ba4 --- /dev/null +++ b/voc-onboarding/voc-data-pull/references/platform-recipes.md @@ -0,0 +1,322 @@ +# VoC platform pull recipes + +Per-platform endpoints, pagination, discovery steps, and unified-template field mappings. +Evidence levels: **live-verified** (probed through the real Connect proxy on a real +account), **registry-verified** (paths and mechanics confirmed in the Builder integration +registry's curated, live-probe-informed examples - not probed on a customer account here), +and **doc-grounded** (provider docs, unprobed - verify with a bounded call before +promising data). Recipes are best-effort guidance, not law: the live API is the truth, +adaptation is expected, and a stale recipe must never stop a pull (see the skill's Step 2 +mandate). Registry slugs are the Builder integration registry's; use them as `--app` +values and as the `/agent/brain/<workspace>/data-sources/voc/<platform>/` folder name. A VoC +platform with no section in this file is still in scope: pull it through the skill's no-recipe path +(Step 2) - live API, unified template, same boundaries. + +Pagination defaults for every platform: page size 100 (or the platform max), and a +client-side date cutoff on the item's created date except where a server-side bound +exists - each recipe states its own date bound; prefer a server-side bound over a +client-side cutoff wherever the recipe has one. + +**The coverage contract (window, page-cap resume, "coverage stopped at" reporting) is +owned by the skill** - `SKILL.md`'s hard boundaries and Step 4 report rules. Recipes only +state each platform's mechanics; they never redefine coverage. + +--- + +## judge_me (Pipedream OAuth) - endpoint live-verified, payload doc-grounded + +- Base: `https://judge.me/api/v1` +- List reviews: `GET /reviews?page=1&per_page=100` - `per_page` max 100, page-numbered; + iterate `page` until a short page. +- Product-scoped: `GET /reviews?product_id={judgeMeProductId}&page=1`. Resolve a Judge.me + product id from a Shopify product id via `GET /products/-1?external_id={shopifyProductId}`. +- Date bound: none on the API - page through and cut off client-side by `created_at`. +- Field mapping: `rating` <- `rating` (1-5 int); body <- `body`; `title` <- `title`; + `product_ref` <- `product_external_id` (Shopify product id; `product_handle` also exists); + `author_name` <- `reviewer.name`; `created_at` <- `created_at`; `verified` <- `verified`; + media in `pictures[]` is not carried into the file; `source_url` <- null (the list payload + carries no permalink). + +## trustpilot (Pipedream OAuth, keys in connect modal) - registry-verified paths, verify on first connect + +Use relative paths through the proxy (the connected account carries the API host). Two-step: + +1. Discovery: `GET /v1/business-units/find?name={domain}` (`name` = the website domain, + required) -> `businessUnitId`; or `GET /v1/business-units/search?query={partialName}`. + Resolve once and reuse. +2. List: `GET /v1/business-units/{businessUnitId}/reviews?perPage=100&page=1` (public + reviews; `perPage` max 100; `stars`, `language`, `orderBy` filters available). Private + reviews with consumer details live at + `/v1/private/business-units/{businessUnitId}/reviews` and need the business-user OAuth + token - verify the grant on connect. + +- Date bound: none - client-side cutoff. +- Field mapping: `rating` <- `stars`; body <- `text`; `title` <- `title`; + `author_name` <- `consumer.displayName`; `created_at` <- `createdAt`; `companyReply` is not + carried into the file; `source_url` <- a `links`/review-URL field if the payload carries one + (verify on first connect), else null. +- **`product_ref` is always null**: Trustpilot core is company-level reviews. Product reviews + are a separate API surface - verify grant coverage before using it. + +## yotpo (Pipedream OAuth) - API mechanics live-verified (proxy path still unverified: plan-gated connect) + +- Discovery: every call needs the per-account `{appKey}` - asked for in the connect modal + (App Key), carried by the connected account. +- List: `GET https://api.yotpo.com/v1/apps/{appKey}/reviews?count=100&page=1`. **Reviews + return oldest first**; sort/direction params are silently ignored - page forward until a + page repeats/empties. +- Date bound (live-verified): `since_date=YYYY-MM-DD` filters on creation date - use it for + backfill and incremental. `updated_at_min` does NOT work. `since_updated_at` works but + sorts by id ascending and mixes in older recently-edited reviews - if you use it to catch + edits, filter client-side for genuinely new items. +- Field mapping: `rating` <- `score`; body <- `content`; `title` <- `title`; + `author_name` <- `user.display_name`; `created_at` <- `created_at`; `product_ref` <- `sku`; + `verified` <- `verified_buyer`; votes are not carried into the file; `source_url` <- null (no + permalink in the list payload). + +## junip (keys-auth in Pipedream) - live-verified against a customer account + +- Connect asks for the Store Key (needs-setup). Base `https://api.juniphq.com`; verify the + key with `GET /v1/stores` (also returns store rating average + distribution). +- List: `GET /v1/product_reviews` (newest first) and `GET /v1/store_reviews`. **v1 only - + v2 paths 404 through this connection.** +- Pagination (live-verified): cursor via `?page[after]={cursor}`, taking the value from + `meta.page.after`; null cursor = last page. Pages are fixed at 50 rows - page-size params + are ignored. +- **No server-side date or product filter - every filter param tested is ignored.** Pull + pages newest-first and cut client-side on the review's created date; filter by product id + client-side too. +- Field mapping: `rating` <- rating; body <- review body; `title` <- title; + `author_name` <- **null on payloads (display names are not returned)** - put the stable + customer id in `custom` instead; `created_at` <- created; `verified` <- verified-buyer + flag; `product_ref` <- product id; `source_url` <- null. + +## okendo (secrets path - NOT in Pipedream's catalog) + +- Auth: customer API key stored as a secret; call with `secure-fetch`. "Connect" for Okendo + means storing a key. +- Discovery: the store id. +- List: `GET https://api.okendo.io/v1/stores/{storeId}/reviews` - cursor-paginated. +- Field mapping (doc-grounded; confirm names against a real key): `rating` <- `rating`; + body <- `body`; `title` <- `title`; `product_ref` <- `productId`; + `author_name` <- reviewer name field; `created_at` <- `dateCreated`; + `verified` <- verified status field; `source_url` <- null (no permalink documented). + +## stamped (secrets path - NOT in Pipedream's catalog) + +- Auth: customer API key + storeHash, via `secure-fetch`. +- List: `GET /api/v2/dashboard/reviews?storeHash=...` (dashboard API). Pagination: + page-numbered via a `page` param (doc-grounded - verify against a real key). +- Field mapping (doc-grounded; confirm against a real key): `rating` <- `reviewRating`; + body <- `reviewMessage`; `title` <- `reviewTitle`; `product_ref` <- `productId` + (`productTitle` also exists); `author_name` <- `author`; `created_at` <- `dateCreated`; + `verified` <- `reviewVerifiedType`; `source_url` <- null (no permalink documented). + +## gorgias (registry: `gorgias_oauth`) - live-verified. Support conversations, not reviews. + +- `source_type: support_conversation`. +- List: `GET /api/tickets?limit=N&order_by=updated_datetime:desc` - cursor pagination via + `meta.next_cursor` (observed live). `GET /api/account` verifies the connection. +- Messages: fetch per ticket with `GET /api/messages?ticket_id=...` so the file body carries + the full conversation. +- Date bound: none - newest-first ordering plus client-side cutoff. +- Field mapping: `title` <- `subject`; `status` <- `status`; `channel` <- `channel`/`via`; + `tags` <- `tags[]` names; `author_name` <- `customer.name`; `created_at` <- + `created_datetime`; `updated_at` <- `updated_datetime`; `reply_count` <- + `messages_count - 1` (`messages_count` includes the root message; `reply_count` counts + messages beyond it); `custom` <- `custom_fields` (pass through as-is - this is where + org-specific headers like Category/Detail/Customer tier come from); `rating` <- CSAT when + available: `GET /api/satisfaction-surveys?limit=100` returns scores and comments once + customers respond - join by ticket id (the ticket object itself carries no CSAT); + `source_url` <- construct `https://<account domain>/app/ticket/<id>` + from the domain in the `/api/account` response - null if the domain is unknown. + +## intercom (Pipedream OAuth) - live-verified. Conversations + CSAT, not reviews. + +- `source_type: support_conversation`. +- List: `GET /conversations?per_page=N` - cursor pagination via `pages.next.starting_after` + (plus `total_count`). Send the pinned `Intercom-Version` header the registry documents. +- Date-bounded pulls: `POST /conversations/search` (live-verified) is the date-boundable + path - prefer it for bounded pulls. +- Messages: conversation parts, fetched per conversation. +- Field mapping: `title` <- `source.subject`; body <- `source.body` plus conversation parts; + `status` <- `state`; `author_name` <- `source.author` / contact name; + `created_at`/`updated_at` <- `created_at`/`updated_at`; `custom` <- `custom_attributes`; + `rating` <- `conversation_rating` (CSAT - the review-like signal); `channel` from the + source type when present; `reply_count` <- count of conversation parts beyond the source + message; `source_url` <- null (inbox permalinks need workspace context the payload does + not carry). +- Intercom workspaces can be large: keep `per_page` small on the first call and stay + deliberate about pull size. + +## reddit (Pipedream OAuth) - doc-grounded, verify on first connect. Community posts, not reviews. + +- `source_type: community_post`, platform folder `reddit`. +- **Connection:** Pipedream OAuth, needs-setup (the connect modal asks for a Reddit Client + ID/Secret). Base `https://oauth.reddit.com`; always add `raw_json=1` to reads. +- **Pull targets are org-specific - confirm before the first pull**: which subreddits + (brand's own sub, category subs) and/or search queries (brand name, product names). + Candidates: `GET /r/{subreddit}/new?limit=100&raw_json=1`, + `GET /r/{subreddit}/search?q={query}&restrict_sr=1&sort=new&limit=100&raw_json=1`, + `GET /search?q={query}&restrict_sr=0&sort=new&limit=100&raw_json=1`. +- **Pagination:** cursor via fullname `after` tokens (`limit` max 100). Items are under + `data.children[].data`; the next cursor is `data.after`; stop when `data.after` is null - + NOT when a page looks short. +- **Coverage contract: the full 12-month window, worked around Reddit's listing limit.** + Reddit's API refuses to return more than ~1000 items per listing (platform-imposed, not + ours). Do not accept 1000 as the coverage: slice the window instead - pull per listing + (subreddit new, top with time filters, search with date-bounded queries) and combine, so + each slice stays under the limit and the whole 12 months gets covered. Only if slicing + still cannot reach older in-window items, report the exact gap (dates not reachable and + why) instead of claiming the window is covered. +- Date bound: none server-side on listings - `sort=new` plus client-side cutoff on + `created_utc`. +- Field mapping: `title` <- `title` (posts; null for comments); body <- `selftext` (posts) / + `body` (comments); `author_name` <- `author` (username; `author_contact` stays null + regardless); `created_at` <- `created_utc` (epoch -> ISO 8601); + `reactions_total` <- `score`; `reply_count` <- `num_comments` (posts); + `parent_ref` <- parent fullname for comments (comments become their own files); + `source_url` <- `https://www.reddit.com` + `permalink`; subreddit name rides in `custom` + (`{ "subreddit": "..." }`); `rating`, `product_ref`, `verified`, and the support fields + are null. +- Comment trees: pull comments only for in-scope posts and only when the org wants them + (deep trees multiply volume fast); each pulled comment is its own file with `parent_ref`. + +## meta ad comments (Motion native - NOT a Runneth integration) + +- `source_type: ad_comments`, platform folder `meta-ad-comments` - **one file per creative** + (`creative-<creative_asset_id>.md`, the per-creative record), not one file per comment. +- Rides the org's existing Motion Meta connection; no Runneth connect at all. +- Pull: `motion meta creative-comments` (per the motion-cli skill). Per creative asset, max + 50 ids per request; root comments bounded at 1,000 per ad unit (explicitly non-exhaustive - + the tool reports coverage level, gaps, and warnings). Served from Motion's cache, so fresh + comments can lag. Junk comments are filtered by default. **Opt in to replies and + reactions** - the file carries all the engagement the tool can return. Output lands as a + JSON file in the workdir - group it by creative asset id and write one file per creative. +- The payload groups comments by creative: `result.comments[]` is one group per creative + with `creativeAssetId`, `adName`, `previewFileUrl`, `coverage`, `warnings`, `errors`, and + a `comments[]` array whose replies arrive **nested** under their parent comment. +- Mapping, per comment entry: `id` <- `id`; body-section text <- `text`; + `author_name` <- `authorName`; `created_at` <- `createdAt`; `platform` <- `platform` + (facebook/instagram); `reactions_total` <- `reactions.total`; + `reply_count` <- `replyCount`; `parent_ref` <- the parent comment's `id` for entries + flattened out of a `replies` array, null for roots. +- Mapping, creative level: `creative_asset_id` <- `creativeAssetId`; + `preview_file_url` <- `previewFileUrl` (the only URL the payload carries - a creative + preview link, NOT a post permalink; null when absent, never invented); + `custom.ad_name` <- `adName`; `comment_count` <- the number of comments written to the + file; when the group's `coverage.commentsTotal` (the platform-reported cached total) + exceeds it - the 1,000-root bound makes that possible - record it as + `custom.comments_total_reported`. + +--- + +## reviews_io (keys in Pipedream connect modal) - registry-verified paths. Merchant + product reviews. + +- Writes `review` files. Auth is a Pipedream-managed Store ID + API key; **API credentials + exist only on an active Plus plan** - trial and lower plans have none. Static base + `https://api.reviews.io`. +- List: `GET /merchant/reviews?per_page=25&page=1` (company-level) and + `GET /product/review?per_page=25&page=1` (product-level, singular path; `sku={sku}` to + filter one product). Page until a short page. +- Date bound: none documented - newest-first plus client-side cutoff; verify ordering on + first pull. +- Field mapping (doc-grounded): `rating` <- rating; body <- review text; `title` <- title; + `author_name` <- reviewer name; `created_at` <- date; `product_ref` <- sku (product + reviews) / null (merchant); `verified`/`source_url` <- verify against a real payload. + +## zendesk (Pipedream OAuth) - doc-grounded, verify on first connect. Support conversations. + +- Writes `ticket` files. List: `GET /api/v2/tickets` (cursor pagination via + `page[after]`); comments per ticket via `GET /api/v2/tickets/{id}/comments`. +- Date bound: incremental exports support `start_time`; otherwise sort by `updated_at` + desc with a client-side cutoff. Use `updated_at` for the re-pull bound. +- Field mapping (doc-grounded): body <- ticket subject + comment bodies (one `###` section + per comment, in order); `author_name` <- requester name; `created_at` <- `created_at`; + `reply_count` <- comment count - 1; `rating` <- satisfaction rating when present; + `source_url` <- the agent-facing ticket URL. + +## klaviyo (Pipedream OAuth or stored key) - registry-verified. Reviews product. + +- Writes `review` files from Klaviyo Reviews. List: + `GET /api/reviews/?filter=greater-or-equal(created,<ISO>)&sort=-created` - server-side + date bound plus newest-first, use it for both backfill and incremental. The required + `revision` header is sent automatically by Builder through the Pipedream proxy - **on the + stored-secret path you must send it yourself** (`revision: 2026-04-15`, base + `https://a.klaviyo.com`, `Authorization: Klaviyo-API-Key <key>`). +- Pagination: follow the full `links.next` URL from the response - do not build the cursor + yourself. +- Field mapping (doc-grounded): `rating` <- `attributes.rating`; body <- + `attributes.content`; `title` <- `attributes.title`; `product_ref` <- related product id; + `author_name` <- `attributes.author`; `created_at` <- `attributes.created`; + `verified` <- `attributes.verified`; `source_url` <- null. + +## attentive (Pipedream OAuth or stored key) - doc-grounded, verify on first connect. SMS replies. + +- Writes `ticket` files (one conversation per subscriber thread). API surface for message + history is limited - verify what the org's plan exposes before promising data; if only + webhooks exist, report that as a gap rather than polling. +- Field mapping (doc-grounded): body <- message texts in order; `author_name` <- subscriber + phone/name; `created_at` <- message timestamp; `rating`/`verified`/`product_ref` <- null. + +## gong (Pipedream OAuth, one-click) - registry-verified. Recorded customer calls. + +- Writes `ticket` files (one per call; the transcript is the conversation). Gong uses a + per-account API host - use relative `/v2/` paths through the proxy, never a hardcoded + domain. +- List: `GET /v2/calls?fromDateTime=<ISO>&toDateTime=<ISO>` for the simple list; for richer + fields use `POST /v2/calls/extensive` with `{"filter": {"fromDateTime": ...}, + "contentSelector": {"context": "Extended"}}` (cursor rides in the request body). + Transcripts: `POST /v2/calls/transcript` with `{"filter": {"callIds": [...]}}`. +- Date bound: server-side `fromDateTime`/`toDateTime` - use for both backfill and + incremental. +- Field mapping (doc-grounded): body <- transcript turns as `###` sections per speaker; + `author_name` <- external participant name; `created_at` <- call `started`; + `reply_count` <- turn count - 1; `source_url` <- call URL when exposed. + +## hotjar (Pipedream OAuth or stored key) - doc-grounded, verify on first connect. Surveys + feedback. + +- Writes `review` files (a survey/feedback response maps to the review shape: score -> + `rating`, response text -> body). List: survey responses endpoint per site id - verify + the exact path against the org's plan on first connect. +- Field mapping (doc-grounded): `rating` <- score (normalize to the platform's scale noted + in `custom`); body <- open-text response; `created_at` <- response timestamp; + `author_name`/`verified`/`product_ref` <- null unless the survey captures them. + +## discord (Pipedream OAuth) - doc-grounded, verify on first connect. Community posts. + +- Writes `community_post` files, same shape as Reddit. Pull targets (guild + channels) are + org-specific and must be confirmed before the first pull. List: + `GET /channels/{channelId}/messages` (cursor pagination via `before`). +- Date bound: none server-side - client-side cutoff on message timestamp. +- Field mapping (doc-grounded): body <- `content`; `author_name` <- author username; + `created_at` <- `timestamp`; `reactions_total` <- sum of reaction counts; + `parent_ref` <- referenced message id for replies/threads; `source_url` <- message link + (`https://discord.com/channels/<guild>/<channel>/<message>`). + +## youtube (registry: `youtube_data`) - doc-grounded, verify on first connect. Video comments. + +- Writes per-item `comment` files (community-post record, like Reddit comments). List: + `GET /youtube/v3/commentThreads?allThreadsRelatedToChannelId=<channelId>` (cursor + pagination via `pageToken`); replies ride in the thread payload or via `comments.list`. +- Date bound: none server-side on threads - order by time and cut client-side on + `publishedAt`. +- Field mapping (doc-grounded): body <- `textOriginal`; `author_name` <- + `authorDisplayName`; `created_at` <- `publishedAt`; `reactions_total` <- `likeCount`; + `reply_count` <- `totalReplyCount`; `parent_ref` <- parent comment id for replies; + `source_url` <- video URL + comment anchor. + +## Variation summary (what actually changes per platform) + +| dimension | judge_me | trustpilot | yotpo | junip | okendo | stamped | +|---|---|---|---|---|---|---| +| rating field | `rating` | `stars` | `score` | `rating` | `rating` | `reviewRating` | +| text field | `body` | `text` | `content` | `body` | `body` | `reviewMessage` | +| product ref | Shopify product id | **none (company-level)** | `sku` | `product.remote_id` | `productId` | `productId` | +| date bound on API | none (client-side) | none (client-side) | **`since_date`** | none (client-side) | TBD | TBD | +| pagination | page number | page number | page number (oldest-first) | `page[after]` cursor | cursor | page number (doc-grounded) | +| discovery step | none | businessUnitId | **appKey** | none | storeId | storeHash | +| connection path | OAuth registry | OAuth registry | OAuth registry | **secret key** | **secret key** | **secret key** | + +Reviews.io and the non-review platforms (support, engagement, community, Meta ad comments) +are covered by their own recipe sections above rather than this review-platform table. diff --git a/voc-onboarding/voc-data-pull/templates/ad-comments-creative.md b/voc-onboarding/voc-data-pull/templates/ad-comments-creative.md new file mode 100644 index 0000000..cf01336 --- /dev/null +++ b/voc-onboarding/voc-data-pull/templates/ad-comments-creative.md @@ -0,0 +1,77 @@ +# Template: Meta ad-comments file (one per creative) + +Path: `/agent/brain/<workspace>/data-sources/voc/meta-ad-comments/creative-<creative_asset_id>.md` + +Layout: H1 headline, bold-label human header (two trailing spaces end every label line), +every comment on the creative between `---` rules - one `###` section per comment, newest +root first, each reply directly under its parent - then the collapsed metadata block. + +The creative is the unit: a sync run that pulls comments for a creative regenerates this +file whole (full comment set, updated totals and engagement), never appends. Comment text +lives only in the body sections; the metadata block carries one entry per comment without +the text. `author_contact` stays null (PII policy pending). Raw platform payloads are never +written into the file. + +````markdown +# Ad comments - creative 120212345678901234 + +**Creative asset id:** 120212345678901234 +**Preview file URL:** — +**Ad name:** UGC-hiking-testimonial-v3 +**Total comments:** 2 +**Newest comment:** 2026-07-15 + +--- + +### jess.outdoors - 2026-07-15 (41 reactions, 1 reply) + +Bought this after seeing the ad three times. It actually holds up on long hikes - wish I'd +found it years ago. + +### trailco - 2026-07-15 (in reply to jess.outdoors, 3 reactions) + +That's what we love to hear - happy hiking! + +--- + +<details> +<summary>Metadata (per-creative ad-comments record)</summary> + +```yaml +source_platform: meta-ad-comments +source_type: ad_comments +creative_asset_id: "120212345678901234" +preview_file_url: null +comment_count: 2 +newest_comment_at: "2026-07-15T18:40:12Z" +oldest_comment_at: "2026-07-15T18:22:05Z" +custom: + ad_name: "UGC-hiking-testimonial-v3" +comments: + - id: "17912345678901234" + created_at: "2026-07-15T18:22:05Z" + author_name: "jess.outdoors" + author_contact: null + platform: instagram + reactions_total: 41 + reply_count: 1 + parent_ref: null + - id: "17912345678905678" + created_at: "2026-07-15T18:40:12Z" + author_name: "trailco" + author_contact: null + platform: instagram + reactions_total: 3 + reply_count: 0 + parent_ref: "17912345678901234" +``` + +</details> +```` + +Field names map 1:1 from the pull's payload (the recipe in +`references/platform-recipes.md` spells out the mapping): `preview_file_url` is the +payload's `previewFileUrl` - the only URL it carries, a creative preview link, not a post +permalink. Every other group-level field the payload returns (`adName`, the +coverage-reported cached total when it exceeds `comment_count`) rides in `custom` - +capture everything the tool gives, invent nothing. diff --git a/voc-onboarding/voc-data-pull/templates/community-post.md b/voc-onboarding/voc-data-pull/templates/community-post.md new file mode 100644 index 0000000..1e48788 --- /dev/null +++ b/voc-onboarding/voc-data-pull/templates/community-post.md @@ -0,0 +1,59 @@ +# Template: community post file (Reddit) + +Paths: `/agent/brain/<workspace>/data-sources/voc/reddit/post-<external_id>.md` for posts, +`/agent/brain/<workspace>/data-sources/voc/reddit/comment-<external_id>.md` for pulled comments +(each comment is its own file with `parent_ref` set - never nested blobs). + +Every metadata field is always present; `null` when the source lacks the concept. The +subreddit rides in `custom`. `author_name` is the Reddit username; `author_contact` stays +null. Raw platform payloads are never written into the file. + +````markdown +# Reddit post — "Anyone else's strapless actually stay up?" + +**Subreddit:** r/braadvice +**Author:** u/wardrobe_wins +**Date:** 2026-07-10 +**Upvotes:** 214 +**Replies:** 37 + +--- + +Serious question - I've bought four different strapless bras this year and every single +one slides by mid-afternoon. Saw an ad for one that claims it grips differently. Has +anyone actually found one that survives a full workday? + +--- + +<details> +<summary>Metadata (unified VoC record)</summary> + +```yaml +source_platform: reddit +source_type: community_post +external_id: "t3_1abcd2e" +created_at: "2026-07-10T16:41:22Z" +title: "Anyone else's strapless actually stay up?" +author_name: "wardrobe_wins" +author_contact: null +reply_count: 37 +parent_ref: null +source_url: "https://www.reddit.com/r/braadvice/comments/1abcd2e/anyone_elses_strapless_actually_stay_up/" +rating: null +product_ref: null +verified: null +status: null +channel: null +tags: null +updated_at: null +custom: + subreddit: "braadvice" +reactions_total: 214 +``` + +</details> +```` + +A pulled reply is a separate file (`comment-<id>.md`) with `title: null`, +`parent_ref: "t3_1abcd2e"`, an `**In reply to:**` header label, its own `reactions_total` +(comment score), and the comment text as the body. diff --git a/voc-onboarding/voc-data-pull/templates/review.md b/voc-onboarding/voc-data-pull/templates/review.md new file mode 100644 index 0000000..97d4a64 --- /dev/null +++ b/voc-onboarding/voc-data-pull/templates/review.md @@ -0,0 +1,58 @@ +# Template: review file + +Path: `/agent/brain/<workspace>/data-sources/voc/<platform>/review-<external_id>.md` + +Layout: H1 headline with stars (omit the quoted title when null), bold-label human header +(two trailing spaces end every label line), the review text between `---` rules, then the +collapsed metadata block. Every metadata field is always present; `null` when the source +lacks the concept. `author_contact` stays `null` until the PII policy call is made, and raw +platform payloads are never written into the file. `source_url` comes only from the +recipe's mapping - Judge.me's list payload has no permalink, so it is null here. + +````markdown +# Review #31274522 — "Finally something that works" ★★★★★ + +**Platform:** Judge.me +**Rating:** 5/5 +**Reviewer:** Dana M. +**Date:** 2026-06-14 +**Product:** 8641242349791 +**Verified buyer:** Yes + +--- + +Finally something that works. I'd given up on strapless options entirely until a friend +recommended this - wore it for a full wedding day and forgot I had it on. + +--- + +<details> +<summary>Metadata (unified VoC record)</summary> + +```yaml +source_platform: judge_me +source_type: review +external_id: "31274522" +created_at: "2026-06-14T09:12:44Z" +title: "Finally something that works" +author_name: "Dana M." +author_contact: null +reply_count: 0 +parent_ref: null +source_url: null +rating: 5 +product_ref: "8641242349791" +verified: true +status: null +channel: null +tags: null +updated_at: null +custom: null +reactions_total: null +``` + +</details> +```` + +For a shop-level review (no product) the Product label reads `**Product:** — (shop-level +review)` and `product_ref` is null. For a 1-star review the headline stars are `★☆☆☆☆`. diff --git a/voc-onboarding/voc-data-pull/templates/support-conversation.md b/voc-onboarding/voc-data-pull/templates/support-conversation.md new file mode 100644 index 0000000..53e0c0d --- /dev/null +++ b/voc-onboarding/voc-data-pull/templates/support-conversation.md @@ -0,0 +1,90 @@ +# Template: support conversation file + +Path: `/agent/brain/<workspace>/data-sources/voc/<platform>/ticket-<external_id>.md` +(keyed by ticket id so re-pulls overwrite the same file). + +Layout: H1 headline, bold-label human header (two trailing spaces end every label line; +every `custom` key becomes its own label - that is where org-specific lines like +`Category` / `Customer tier` come from), the full conversation between `---` rules (one +`###` section per message, in order), then the collapsed metadata block. + +Every metadata field is always present; `null` when the source lacks the concept. `custom` +passes platform custom fields through as-is (Gorgias `custom_fields`, Intercom +`custom_attributes`) - do not rename or enumerate its keys. `reply_count` counts messages +beyond the root (`messages_count` 4 -> `reply_count` 3). `author_contact` stays null (PII +policy pending) and raw platform payloads are never written into the file. + +````markdown +# Ticket #88213307 — Re: Order 4821 arrived with the wrong size + +**Platform:** Gorgias +**Date:** 2026-07-18 +**Status:** Closed +**Channel:** Email +**Customer:** Priya S. +**Category:** Order issue +**Detail:** Wrong size shipped +**Customer tier:** Repeat +**Tags:** sizing, csat_excluded +**Messages:** 4 (last activity 2026-07-19) + +--- + +### Priya S. (customer) — 2026-07-18 14:02 + +Hi - my order 4821 arrived today but it's a medium, I ordered a small. Can you swap it? + +### Support (agent) — 2026-07-18 15:30 + +So sorry about that, Priya! I've set up a replacement in a small shipping out today - +keep or donate the medium, no return needed. + +### Priya S. (customer) — 2026-07-19 10:02 + +That's amazing, thank you! You've made a customer for life. + +### Support (agent) — 2026-07-19 10:44 + +So happy to hear it, Priya! Closing this out - reach back any time. + +--- + +<details> +<summary>Metadata (unified VoC record)</summary> + +```yaml +source_platform: gorgias_oauth +source_type: support_conversation +external_id: "88213307" +created_at: "2026-07-18T14:02:11Z" +title: "Order 4821 arrived with the wrong size" +author_name: "Priya S." +author_contact: null +reply_count: 3 +parent_ref: null +source_url: "https://example.gorgias.com/app/ticket/88213307" +rating: null +product_ref: null +verified: null +status: "closed" +channel: "email" +tags: + - "sizing" + - "csat_excluded" +updated_at: "2026-07-19T10:44:03Z" +custom: + Category: "Order issue" + Detail: "Wrong size shipped" + Customer tier: "Repeat" +reactions_total: null +``` + +</details> +```` + +Notes: + +- For Intercom, `rating` carries the CSAT (`conversation_rating`) when present - add a + `**CSAT:**` label to the header when it is populated. +- Support tickets live over time: re-pulls overwrite the ticket file with the fresher + `updated_at`, header, and any new messages. diff --git a/voc-onboarding/voc-onboarding-walkthrough/SKILL.md b/voc-onboarding/voc-onboarding-walkthrough/SKILL.md new file mode 100644 index 0000000..3f391c7 --- /dev/null +++ b/voc-onboarding/voc-onboarding-walkthrough/SKILL.md @@ -0,0 +1,167 @@ +--- +name: voc-onboarding-walkthrough +description: The entry point for Voice of Customer onboarding — finds the org's reachable VoC integrations, sets up the recurring syncs into the workspace's brain folder, and, once data lands, presents the per-integration Voice of Customer summary and offers the Voice of Customer Audit. Invoke when someone says "run voice of customer onboarding", "start VoC onboarding", "set up voice of customer", asks to begin or resume their Voice of Customer onboarding, or asks whether their customer-voice data is ready. Never self-runs at install or on a schedule. +--- + +# Voice of Customer Onboarding Walkthrough + +This skill owns the Voice of Customer onboarding sequence for one Motion workspace: a +**setup phase** (find what the org can reach, pin accounts, create the recurring syncs) and +a **presentation phase** (say what customer voice the brain holds, then offer the Voice of +Customer Audit). Nothing in the voc-onboarding package runs before a person invokes this +skill or asks for one of its parts by name — installing the package stages files and does +nothing else. The sync mechanics live in the voc-data-pull skill and the audit lives in +the voc-audit skill; this skill orchestrates and presents. + +## Step 0 — resolve the workspace first + +Everything this sequence persists lives in one folder per Motion workspace: +`/agent/brain/<workspace>/`, where `<workspace>` is the name of the workspace this +conversation operates in, slugged - lowercase, every run of characters that is not a-z or +0-9 becomes one hyphen, trim leading and trailing hyphens ("Bramblewick NYC" -> +`bramblewick-nyc`, "St. Fig & Co." -> `st-fig-co`). + +This is the sequence's first action, before any other work: no Motion commands, no routine +listing, no brain-file reads until it is done. This system prompt's `Motion context:` +section contains a `Default workspace:` line stating the workspace name and workspaceId +the runtime bound to this conversation - the same workspace every bare `motion` command +resolves to when `--workspace-id` is omitted, so it cannot disagree with where the data +actually lives. Quote that line verbatim, exactly as it appears in this prompt, then state +the slug derived from its name. Those three values - name, workspaceId, slug - are the +workspace for every later step, and nothing downstream may re-resolve them from anything +else. If that line is null or absent, ask which workspace to onboard and stop until +answered - never guess. + +Nothing else identifies the workspace: not existing `/agent/brain/<workspace>/` folders, +not `voc-sync-<workspace>-*` routine names, not a prior run or another conversation, not +remembered context or anything memory or a brain search returns - those record whichever +workspaces onboarded *earlier*, and on a multi-workspace VM another workspace's state is +always present. Another workspace's folders and routines are also not a resume signal: +whether this run is a fresh setup or a resume is decided only after step 0 resolves the +workspace, and only by the resolved workspace's own routines and files. One org VM holds +several workspace folders side by side and they never merge: a second workspace onboarding +is normal, not a conflict. + +VoC files live under `/agent/brain/<workspace>/data-sources/voc/<platform>/`. The later +Voice of Customer audit skill writes +`/agent/brain/<workspace>/data-sources/voc/voice-of-customer-audit.md` after raw VoC data +has landed. This skill and the raw sync routines do not create it; its initial absence is +expected. + +## Phase 1 — setup (runs when this workspace has no `voc-sync-<workspace>-*` routines yet) + +1. **Check what the org can reach.** `integrations status --app <slug>` for each known + VoC platform slug (the voc-data-pull skill's Step 1 table lists them; `integrations + list --query <term>` finds any others) for OAuth connections, plus the stored secrets + for **every** VoC platform (any platform may be key-stored instead of connected), plus + whether a Meta workspace is connected. The secret store cannot be listed - the runtime + refuses Bash reads of `/agent/.runtime/secrets`, and neither `secret` nor + `secure-fetch` has a list command. The only probe that counts is running each + key-stored platform's documented secret key: `secret run --env KEY=<SECRET_KEY> -- true` + (or that platform's bounded `secure-fetch run` from the skill). A "secret not + available" error means not stored. A refused `ls`, a `--help` read, or any other + indirect check proves nothing and is never grounds to mark a platform unreachable - + every key-stored platform in the skill's table gets its key probed this way before + this step is done. VoC scope is customer-voice data, not the skill's recipe list - a + reachable reviews/support/community platform with no recipe still counts. Integrations + and stored secrets are VM-wide, so a platform reachable for one workspace is reachable + here too; what changes per workspace is where its data lands. +2. **Set up the recurring syncs (they run in the background).** For each reachable VoC + platform, run the voc-data-pull skill's "Set up the recurring sync" procedure: pin the + platform account to this workspace, create the `voc-sync-<workspace>-<platform>` + routine, and kick its first run. The pin is the skill's step 1 and it can need a human + answer - accounts are org-level with no workspace tag, so which account belongs to + this workspace is never inferred. Handle that inside this invocation: platforms the + skill lets you auto-pin (the org has exactly one Motion workspace) get their routine + created and kicked now; for the rest, ask the skill's confirmation question for every + pending platform in one compact block just before the status report, mark those + platforms "waiting on a person - account confirmation" in the report's VoC line, and + create and kick their routines the moment the answer arrives - in that follow-up turn, + never before. A routine is never created on an unconfirmed account just to keep the + backfill moving. The workspace belongs in the routine name because routines are + VM-wide - `voc-sync-gorgias` would collide with another workspace's routine, and a + collision is what mixes two brands' customer data into one corpus. For the same reason + the routine's script carries this workspace's folder path, workspace id, and pinned + account id **literally**, never "resolve the current workspace" or "the connected + account": routine conversations run with no workspace attached, so a routine that + tries to resolve one at run time has nothing to resolve. Its output path is + `/agent/brain/<workspace>/data-sources/voc/<platform>/`, written out in full. + **A connected Meta workspace is itself a reachable VoC platform** - ad comments are + customer voice, pulled with `motion meta creative-comments` (skill slug + `meta-ad-comments`; one file per creative under `voc/meta-ad-comments/`, at the same + level as the other platform folders) - so it always gets a + `voc-sync-<workspace>-meta-ad-comments` routine alongside the others: the standard + pull of every onboarding, not a discovery outcome. For Meta, connected is the only + reachability test: if a Meta workspace shows as connected, create and kick that + routine even when a Meta API probe errors in this conversation - the routine's own + scheduled runs absorb transient API failures. An API error is never grounds to skip + the routine; only the absence of a connected workspace is. **Every routine created in + this step gets its first run kicked before moving on - check them off one by one.** + The 12-month backfills churn in the background. Never pull VoC data inside this + conversation. If old canceled `voc-sync-*` routines exist from a previous run, ignore + them - canceled is terminal; never resume or reuse one, always create fresh. Leave + other workspaces' `voc-sync-*` routines alone. +3. **Close with a status report - status only, never content.** One line per platform + stating its state (syncing in background / waiting on a person - account confirmation / + skipped and why), plus one line for the audit. No counts, no findings, no file paths. + The shape is literal: + + > voc-onboarding - setup complete for <workspace> + > - VoC sync: <per-platform status, one line total> + > - Voice of Customer Audit: waits for backfill completion and a person's yes + > + > I'll have your Voice of Customer summary once the backfills complete - ask me to + > run your Voice of Customer onboarding again anytime to check in. + + If nothing is reachable at all: say so and stop. Do not watch or poll; when a platform + is connected later, this skill runs setup for it on ask. + +## Phase 2 — presentation (runs when this workspace's syncs already exist) + +- **Present the Voice of Customer summary — proactively, not on request.** Inspect this + workspace's platform folders under `/agent/brain/<workspace>/data-sources/voc/` and its + `voc-sync-<workspace>-*` routines, then tell the person what customer voice the brain + actually holds: one line per integration — the platform, what kind of voice it carries, + how many items are synced, how many products they span, and the date coverage. For + example: "Judge.me: 1,240 reviews across 6 products, May 2025 – July 2026." If a + backfill is still running, present the counts so far and say the sync is still filling + in. If a reachable platform has no routine yet (connected after setup ran), run Phase 1 + for that platform first. +- **Then offer the Voice of Customer Audit by previewing the plan, in Runneth's own words — + never a script.** The offer walks through what the audit will actually do with this + workspace's data: now that the reviews and comments are in, Runneth would like to run an + audit — it will separate every entry by product, score each 1–5 for usefulness, and break + the strong ones into five buckets, named plainly (pain points — what was wrong before + they bought; trigger moments — what actually made them pull the trigger; objections — + what nearly stopped them; transformations — what changed after; standout language — the + best verbatim lines kept in one swipe file), plus evidence-backed personas for each + product with 200 or more entries, saved as one compiled page the brain reads for + customer-side WHY questions. Close the preview by handing the plan to the person: would + they like anything added, and do they have existing docs to use as reference (existing + personas especially)? Then the trigger is theirs: + - **Data ready, no audit yet:** check whether + `/agent/brain/<workspace>/data-sources/voc/voice-of-customer-audit.md` exists; if not, + make the offer above. If `/agent/brain/<workspace>/_changelog.md` has no + `voc-audit-offer` entry, append a dated one. A yes invokes the `voc-audit` skill, + carrying any additions and reference docs the person named. + - **An audit already exists:** say when it last ran and roughly how much new customer + voice has synced since, and offer a rerun instead. + - **Backfill incomplete or under 200 entries:** still present the summary and the + preview, then say the audit will be ready when coverage completes — never start it + against a partial backfill. + + The audit runs only on a person's yes here or an explicit later request — never because + the walkthrough completed. + +## Resume and idempotency + +- Setup is done for a platform when its `voc-sync-<workspace>-<platform>` routine exists: + skip it (same pinned account, no re-confirmation) and set up only what is missing. + Re-invoking this skill is always safe — an invocation where every reachable platform + already has its routine goes straight to Phase 2. +- The sync routine's own first-backfill note (defined in the voc-data-pull skill) offers + the audit asynchronously when coverage completes; this skill's Phase 2 is the on-demand + surface for the same offer. Both check `/agent/brain/<workspace>/_changelog.md` for an + existing `voc-audit-offer` entry, so the person is never offered twice by default. +- Never read, copy, rename, or overwrite another workspace's folder or routines to serve + this one.