Plan overview
Voice of Customer Research
Scan recent customer calls for a target keyword, extract the exact quotes around it, synthesize an opinionated answer to a research question, and publish it to Google Drive.
Best for
Product marketing · Product · Founders
Runs with
Avoma · Google Drive · Keyword research
At a glance
04
Outputs
01
Capabilities
03
Teams
Core outputs
Executive summary
Key themes
Supporting quotes
Shared Drive doc
Capability requirements
What it solves
The truth about a feature or objection is buried across dozens of call transcripts, and reading them one by one doesn't scale.
Turns hours of transcript reading into one structured research pass.
Grounds every claim in verbatim customer quotes with links.
Publishes a shareable artifact instead of a one-off answer.
Workflow
Fetch recent external meetings and locate keyword mentions per transcript.
Extract the verbatim quotes and surrounding context around each mention.
Synthesize a sharp, evidence-backed answer and push it to Google Drive.
Implementation
Review the underlying FML plan — the sessions, tools, and typed schemas that define this workflow — and see exactly how it is instructed for repeatable execution.
FML Plan
voc.fml
187 lines
system("You are an elite Product Marketing Manager and senior Voice of the Customer analyst for Barndoor AI. Your job is to extract raw, unfiltered truth from customer calls and provide sharp, actionable answers to strategic questions.") parameter("keyword", type=string, title="Target Keyword to Scan For") parameter("question", type=string, title="Research Question") parameter("max_meetings", type=int, default=10, title="Max Meetings to Analyze") parameter("attendee_email", type=string, title="Attendee Email") require mcp Avoma require mcp Google-Drive # ── Session 1: Fetch meetings ───────────────────────────────────────────────── session("fetch_meetings") { use mcp Avoma + Call get_current_datetime to get the current UTC time. Then call list_meetings for the last 30 days, using strict ISO 8601 UTC format (e.g. YYYY-MM-DDThh:mm:ssZ) for from_date and to_date. Use attendee_emails: ["{{ .params.attendee_email }}"] Paginate if needed, up to {{ .params.max_meetings }} total meetings. Only include meetings where a transcript or recording is available. - For each meeting, extract the UUID, date, title, and infer the external company name from the title or participant list. Exclude internal only meetings. Limit output strictly to {{ .params.max_meetings }} meetings. schema { meetings: { uuid: string date: string title: string company: string # External company name — infer from title or attendees }[] } } # ── Session 2: Read transcript and extract relevant excerpts ────────────────── session("read_transcript", after="fetch_meetings", expect="len(context.fetch_meetings.meetings) > 0", iterate="context.fetch_meetings.meetings") { # 1. Fetch the full transcript explicitly via PreCall call("get_meeting_transcript") -> transcript { uuid = "{{ .it.uuid }}" } # 2. Use pure JS to find the keyword and slice out only the relevant surrounding text call("extract_snippets") -> snippets { keyword = "{{ .params.keyword }}" transcript = $(vars.transcript) code( (() => { const kw = args.keyword.toLowerCase(); const t = typeof args.transcript === 'string' ? args.transcript : JSON.stringify(args.transcript); let result = ""; let searchIdx = 0; let matches = 0; // Find up to 5 occurrences of the keyword and grab a window of text around each while (matches < 5) { const idx = t.toLowerCase().indexOf(kw, searchIdx); if (idx === -1) break; const start = Math.max(0, idx - 1000); const end = Math.min(t.length, idx + 1000); result += "\n\n... " + t.substring(start, end) + " ...\n"; searchIdx = idx + kw.length + 1000; matches++; } return result === "" ? "Keyword not found in this transcript." : result; })(); ) } # 3. Inject ONLY the small snippets into the LLM context, avoiding the 504 timeout! context "Transcript snippets containing the keyword:\n{{ .vars.snippets }}" - You are reading extracted transcript snippets from a meeting between Barndoor AI and {{ .it.company }} on {{ .it.date }}. Meeting URL: https://app.avoma.com/meetings/{{ .it.uuid }} Transcript URL: https://app.avoma.com/meetings/{{ .it.uuid }}/transcript The snippets below contain the target keyword: "{{ .params.keyword }}" Read the surrounding conversation in the snippets to see if it helps answer this specific question: "{{ .params.question }}" If the extracted text provides relevant information, extract the quote and context. If the keyword was used in a way that doesn't help answer the question, return empty arrays. Do not force matches. Be sure to output both the standard meeting URL and the transcript URL provided above. schema { company: string date: string meeting_url: string transcript_url: string # URL to the full Avoma transcript relevant_excerpts: { speaker: string quote: string # The exact, raw quote from the transcript snippets context: string # Brief explanation of what was being discussed }[] }[] } # ── Session 3: Synthesize Answer ────────────────────────────────────────────── session("synthesize_answer", after="read_transcript") { context "Transcript excerpts matching keyword '{{ .params.keyword }}': {{ .context.read_transcript | json }}" - You are analyzing the extracted transcript data to answer the user's specific question: "{{ .params.question }}" Write a highly synthesized, opinionated answer based ONLY on the provided excerpts. - Don't just list what was said—synthesize the underlying themes. - Highlight the most powerful quotes to prove your points. - If the transcripts do not contain enough information to confidently answer the question, explicitly state that. schema { question_asked: string target_keyword: string executive_summary: string # A crisp, 1-2 paragraph answer to the question key_themes: string[] # 2-3 main takeaways or patterns discovered supporting_evidence: { company: string quote: string meeting_url: string transcript_url: string relevance: string # Why this quote helps answer the question }[] } } # ── Session 4: Push to Google Drive ─────────────────────────────────────────── session("push_to_drive", after="synthesize_answer") { # Removed `use mcp Google-Drive` to prevent invalid MCP schema from crashing the LLM. # 1. Format the synthesized answer as Markdown in JS call("format_markdown") -> md_content { s = $(context.synthesize_answer) code( (() => { const s = args.s || {}; let md = `# VoC Synthesis: ${s.target_keyword || "N/A"}\n\n`; md += `**Question:** ${s.question_asked || "N/A"}\n\n`; md += `## Executive Summary\n${s.executive_summary || "N/A"}\n\n`; md += `## Key Themes\n`; (s.key_themes || []).forEach(t => { md += `- ${t}\n`; }); md += `\n`; md += `## Supporting Evidence\n`; (s.supporting_evidence || []).forEach(e => { md += `### ${e.company || "Unknown Company"}\n`; md += `> "${e.quote}"\n\n`; md += `**Relevance:** ${e.relevance}\n\n`; if (e.meeting_url) md += `[Meeting Link](${e.meeting_url}) | `; if (e.transcript_url) md += `[Transcript](${e.transcript_url})\n\n`; }); return md; })(); ) } # 2. Call upload_file explicitly as a PreCall call("upload_file") -> uploadResult { name = "VoC Synthesis: {{ .params.keyword }}" parents = "1Lp_fvBgjjVoDK-qPZX-AlpKSjOTn19G4" mimeType = "application/vnd.google-apps.document" content = $(vars.md_content) } context "Upload Result: {{ .vars.uploadResult | json }}" - The file has been successfully uploaded to Google Drive. Extract the returned `id` and `name` from the upload result context. CRITICAL WARNING: The upload result context includes the parent folder ID (`1Lp_fvBgjjVoDK-qPZX-AlpKSjOTn19G4`). DO NOT extract this folder ID as the file ID. You MUST extract the unique `id` generated for the new file. Then, construct the `file_url` using this format: https://docs.google.com/document/d/<id>/edit schema { file_id: string file_name: string file_url: string } }