Read every call, not the three you had time for
The honest answer about a feature or an objection is spread across dozens of transcripts. Reading them one at a time doesn't scale, and the tools holding them can usually only be queried by attendee or date — never by what was actually said.
What breaks when you do this by hand
The truth about a feature or objection is buried across dozens of transcripts, and reading them one by one doesn't scale.
Ad-hoc questions stall because the underlying tools can only be queried by attendee or date, not by topic.
Findings arrive as prose nobody can aggregate or trend over time.
What you get instead
Topic-level answers across an entire call corpus
Objections and feature signals returned as structured data
The same analysis re-runnable next quarter, unchanged
This is the whole thing
The opening of Voice of Customer Research — the system prompt, its typed parameters, and the tools it's allowed to reach, all declared up front. No canvas, no hidden nodes. 187 lines of source you can review in a pull request.
Read all 187 lines →voc.fml
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 }[] } }
2 plans you can run today
Every one is a typed FML plan. Open it, read the source, and run it — nothing here is a mockup.
Voice of Customer Research
The truth about a feature or objection is buried across dozens of call transcripts, and reading them one by one doesn't scale.
Read the plan
Voice of Customer Chat
Ad-hoc questions about customer calls stall because the underlying tools can only be queried by attendee or date, not by topic.
Read the plan
What every plan here guarantees
Zero prompt drift
Every plan is a versioned contract. Run 1 and run 10,000 behave identically.
Scoped sessions
Each LLM call sees only the context it needs — no one giant prompt, no context rot.
Typed output
Plans return validated objects pinned to a schema, not text you have to parse.
Reusable like an API
Parameterise once and call it from anywhere — versioned, auditable, shareable.