Mavera Surfaces Used
| Surface | Role |
|---|---|
Personas (POST /personas) | Create customer personas from CRM data |
Focus Groups (POST /focus-groups) | Validate next year’s themes, priorities, and messaging with personas |
Generate (POST /generate) | Draft the annual plan sections from validated insights |
Mave Agent (POST /mave/chat) | Research market trends, competitive landscape, and industry benchmarks |
Speak (POST /speak/conversations) | Live voice conversations with personas for qualitative depth |
Chat + response_format | Structure outputs and synthesize across stages |
This is the capstone playbook — it chains every major Mavera surface into one end-to-end workflow. Each stage feeds the next, producing a data-backed annual marketing plan that would normally take weeks of agency work, internal workshops, and executive reviews.
What Value Does Mavera Add?
| Value | How |
|---|---|
| Insurance | Every plan element is validated against personas before it reaches the board deck. No “gut feel” positioning or unvalidated assumptions. |
| Opening new doors | A complete annual plan — from CRM data to finished document — in a single day. Run multiple scenarios (aggressive vs conservative) in parallel. |
| Saving time | Replaces 4-6 weeks of agency research, internal workshops, cross-functional reviews, and document drafting with a single automated pipeline. |
When to Use This
- It’s Q4 and you need next year’s marketing plan with data-backed priorities.
- You’re a new CMO establishing your first-year strategy and need to move fast.
- You’re presenting to the board and need a plan that’s validated, not aspirational.
- You want to compare multiple strategic scenarios (aggressive growth vs efficiency vs expansion).
What You Need
| Requirement | Details |
|---|---|
| Mavera API key | Starts with mvra_live_. Get one at Developer Settings. |
| Workspace ID | From your dashboard URL (ws_...). |
| CRM export or customer data | Customer list with segment, ARR, tenure, and satisfaction data. CSV or JSON. |
| Current year metrics | Revenue, pipeline, win rate, churn, CAC, LTV — for benchmarking. |
| Next year’s candidate themes | 3-5 strategic themes you’re considering (e.g. “Enterprise expansion”, “Product-led growth”, “International markets”). |
| Credits | ~500–1500 total. See Credits Estimate. |
| Python 3.8+ or Node.js 18+ | requests / openai for Python; native fetch for Node. |
MAVERA_API_KEY=mvra_live_your_key_here
MAVERA_WORKSPACE_ID=ws_your_workspace_id
PLAN_YEAR=2027
The Pipeline
This is a 6-stage pipeline where each stage feeds the next:The Flow
1
Analyze CRM data
Parse customer data to identify segments, satisfaction patterns, churn indicators, and growth opportunities. This creates the factual foundation for personas and planning.
2
Create personas from data
Build 5 personas that represent your actual customer base — not hypothetical ICPs, but data-informed profiles of your best, newest, at-risk, churned, and prospect segments.
3
Run Focus Groups on next year's themes
Present 3-5 candidate strategic themes to all personas. Use Ranking, Likert, and Open-Ended questions to validate which themes resonate and which fall flat.
4
Mave researches market context
Use Mave Agent to research market trends, competitive moves, and industry benchmarks that inform the plan. Combines with Focus Group data for grounded strategy.
5
Generate the annual plan
Feed validated themes, Focus Group data, and market research into Generate to draft each section of the annual plan. Structured output ensures consistency.
6
Speak validation conversations
Run live voice conversations with key personas to stress-test the plan. Ask probing questions, handle objections, and refine based on verbal feedback.
Code: Full Annual Planning Pipeline
Setup and Configuration
import os
import json
import time
import csv
import statistics
import requests
from openai import OpenAI
MAVERA_API_KEY = os.environ["MAVERA_API_KEY"]
WORKSPACE_ID = os.environ["MAVERA_WORKSPACE_ID"]
PLAN_YEAR = os.environ.get("PLAN_YEAR", "2027")
BASE = "https://app.mavera.io/api/v1"
HEADERS = {
"Authorization": f"Bearer {MAVERA_API_KEY}",
"Content-Type": "application/json",
}
mavera = OpenAI(api_key=MAVERA_API_KEY, base_url=BASE)
COMPANY = {
"name": "Acme",
"category": "AI-powered market research platform",
"current_arr": "$4.2M",
"growth_rate": "120% YoY",
"team_size": 35,
"key_metrics": {
"customers": 280,
"avg_contract": "$15K",
"win_rate": "32%",
"churn_rate": "8%",
"cac": "$4,200",
"ltv": "$42,000",
"nps": 48,
},
}
CANDIDATE_THEMES = [
{
"theme": "Enterprise Expansion",
"description": (
"Move upmarket: build enterprise features (SSO, SCIM, audit logs), "
"hire enterprise AEs, target companies with 1000+ employees. "
"Goal: 20 enterprise deals at $50K+ ACV."
),
},
{
"theme": "Product-Led Growth",
"description": (
"Launch a self-serve tier with freemium entry. Invest in onboarding, "
"in-app guides, and viral loops. Goal: 1000 self-serve accounts, "
"15% conversion to paid."
),
},
{
"theme": "Content & Thought Leadership",
"description": (
"Become the definitive voice in AI-powered market research. "
"Weekly blog, monthly webinar, quarterly research report, conference presence. "
"Goal: 3x organic traffic, 2x inbound pipeline."
),
},
{
"theme": "Integration Ecosystem",
"description": (
"Build deep integrations with HubSpot, Salesforce, Slack, Notion. "
"Launch a partner program. Goal: 10 integration partners, "
"30% of customers using 2+ integrations."
),
},
{
"theme": "International Markets",
"description": (
"Expand to EU and APAC. Localize product and content. "
"Hire regional marketing leads. Goal: 15% of new ARR from international."
),
},
]
import OpenAI from "openai";
import fs from "fs";
const MAVERA_API_KEY = process.env.MAVERA_API_KEY;
const WORKSPACE_ID = process.env.MAVERA_WORKSPACE_ID;
const PLAN_YEAR = process.env.PLAN_YEAR || "2027";
const BASE = "https://app.mavera.io/api/v1";
const HEADERS = {
Authorization: `Bearer ${MAVERA_API_KEY}`,
"Content-Type": "application/json",
};
const mavera = new OpenAI({ apiKey: MAVERA_API_KEY, baseURL: BASE });
const COMPANY = {
name: "Acme",
category: "AI-powered market research platform",
current_arr: "$4.2M",
growth_rate: "120% YoY",
team_size: 35,
key_metrics: {
customers: 280, avg_contract: "$15K", win_rate: "32%",
churn_rate: "8%", cac: "$4,200", ltv: "$42,000", nps: 48,
},
};
const CANDIDATE_THEMES = [
{
theme: "Enterprise Expansion",
description:
"Move upmarket: build enterprise features (SSO, SCIM, audit logs), " +
"hire enterprise AEs, target companies with 1000+ employees. " +
"Goal: 20 enterprise deals at $50K+ ACV.",
},
{
theme: "Product-Led Growth",
description:
"Launch a self-serve tier with freemium entry. Invest in onboarding, " +
"in-app guides, and viral loops. Goal: 1000 self-serve accounts, 15% conversion.",
},
{
theme: "Content & Thought Leadership",
description:
"Become the definitive voice in AI-powered market research. " +
"Weekly blog, monthly webinar, quarterly research report.",
},
{
theme: "Integration Ecosystem",
description:
"Build deep integrations with HubSpot, Salesforce, Slack, Notion. " +
"Launch a partner program. Goal: 10 integration partners.",
},
{
theme: "International Markets",
description:
"Expand to EU and APAC. Localize product and content. " +
"Goal: 15% of new ARR from international.",
},
];
Stage 1 — Analyze CRM Data
Parse customer data to identify segments and build persona-ready profiles.SAMPLE_CRM_DATA = [
{"name": "TechCorp", "segment": "enterprise", "arr": 85000, "tenure_months": 18, "nps": 9, "usage": "heavy", "features_used": ["focus_groups", "chat", "mave", "video"]},
{"name": "StartupX", "segment": "startup", "arr": 5000, "tenure_months": 4, "nps": 7, "usage": "moderate", "features_used": ["chat", "mave"]},
{"name": "AgencyPro", "segment": "agency", "arr": 24000, "tenure_months": 12, "nps": 8, "usage": "heavy", "features_used": ["focus_groups", "chat", "generate", "brand_voice"]},
{"name": "MidMarket Inc", "segment": "mid_market", "arr": 36000, "tenure_months": 8, "nps": 6, "usage": "light", "features_used": ["chat"]},
{"name": "ChurnedCo", "segment": "churned", "arr": 0, "tenure_months": 6, "nps": 3, "usage": "none", "features_used": []},
]
def analyze_crm_data(data: list[dict]) -> dict:
"""Analyze CRM data to identify segments and patterns."""
segments = {}
for customer in data:
seg = customer["segment"]
if seg not in segments:
segments[seg] = {"customers": [], "total_arr": 0, "nps_scores": [], "tenures": []}
segments[seg]["customers"].append(customer)
segments[seg]["total_arr"] += customer["arr"]
segments[seg]["nps_scores"].append(customer["nps"])
segments[seg]["tenures"].append(customer["tenure_months"])
analysis = {}
for seg, data in segments.items():
analysis[seg] = {
"count": len(data["customers"]),
"total_arr": data["total_arr"],
"avg_arr": data["total_arr"] / len(data["customers"]) if data["customers"] else 0,
"avg_nps": statistics.mean(data["nps_scores"]) if data["nps_scores"] else 0,
"avg_tenure": statistics.mean(data["tenures"]) if data["tenures"] else 0,
"top_features": _most_common_features(data["customers"]),
}
print(f"✓ Analyzed {sum(a['count'] for a in analysis.values())} customers across {len(analysis)} segments")
for seg, info in analysis.items():
print(f" {seg}: {info['count']} customers, ${info['avg_arr']:.0f} avg ARR, NPS {info['avg_nps']:.1f}")
return analysis
def _most_common_features(customers: list[dict]) -> list[str]:
"""Find most commonly used features across customers."""
counts = {}
for c in customers:
for f in c.get("features_used", []):
counts[f] = counts.get(f, 0) + 1
return sorted(counts, key=counts.get, reverse=True)[:5]
const SAMPLE_CRM_DATA = [
{ name: "TechCorp", segment: "enterprise", arr: 85000, tenure_months: 18, nps: 9, usage: "heavy", features_used: ["focus_groups", "chat", "mave", "video"] },
{ name: "StartupX", segment: "startup", arr: 5000, tenure_months: 4, nps: 7, usage: "moderate", features_used: ["chat", "mave"] },
{ name: "AgencyPro", segment: "agency", arr: 24000, tenure_months: 12, nps: 8, usage: "heavy", features_used: ["focus_groups", "chat", "generate", "brand_voice"] },
{ name: "MidMarket Inc", segment: "mid_market", arr: 36000, tenure_months: 8, nps: 6, usage: "light", features_used: ["chat"] },
{ name: "ChurnedCo", segment: "churned", arr: 0, tenure_months: 6, nps: 3, usage: "none", features_used: [] },
];
function analyzeCrmData(data) {
const segments = {};
for (const customer of data) {
const seg = customer.segment;
if (!segments[seg]) segments[seg] = { customers: [], total_arr: 0, nps_scores: [], tenures: [] };
segments[seg].customers.push(customer);
segments[seg].total_arr += customer.arr;
segments[seg].nps_scores.push(customer.nps);
segments[seg].tenures.push(customer.tenure_months);
}
const analysis = {};
for (const [seg, d] of Object.entries(segments)) {
const avg = (arr) => arr.reduce((a, b) => a + b, 0) / arr.length;
analysis[seg] = {
count: d.customers.length,
total_arr: d.total_arr,
avg_arr: d.total_arr / d.customers.length,
avg_nps: avg(d.nps_scores),
avg_tenure: avg(d.tenures),
};
}
console.log(`✓ Analyzed ${data.length} customers across ${Object.keys(analysis).length} segments`);
return analysis;
}
Stage 2 — Create Personas from CRM Data
Build 5 personas that represent real customer segments, not hypothetical ICPs.def generate_persona_descriptions(crm_analysis: dict) -> list[dict]:
"""Use Chat to generate rich persona descriptions from CRM segment data."""
PERSONA_SCHEMA = {"type": "json_schema", "json_schema": {
"name": "personas", "strict": True,
"schema": {
"type": "object",
"properties": {
"personas": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {"type": "string"},
"segment": {"type": "string"},
"title": {"type": "string"},
"description": {"type": "string"},
},
"required": ["name", "segment", "title", "description"],
},
},
},
"required": ["personas"],
},
}}
prompt = (
f"Based on this CRM analysis for {COMPANY['name']} ({COMPANY['category']}), "
f"create 5 detailed personas — one per segment.\n\n"
f"## CRM Analysis\n{json.dumps(crm_analysis, indent=2)}\n\n"
f"## Company Context\n{json.dumps(COMPANY, indent=2)}\n\n"
"For each persona, include:\n"
"- Realistic name and title\n"
"- Company size and budget context\n"
"- How they use (or stopped using) the product\n"
"- Their priorities for next year\n"
"- What would make them increase/decrease their investment\n"
"Make each description 3-5 sentences."
)
resp = mavera.responses.create(
model="mavera-1",
input=[{"role": "user", "content": prompt}],
extra_body={"response_format": PERSONA_SCHEMA},
)
return json.loads(resp.output[0].content[0].text)["personas"]
def create_personas_from_descriptions(persona_descs: list[dict]) -> list[str]:
"""Create personas in Mavera from generated descriptions."""
persona_ids = []
for p in persona_descs:
resp = requests.post(
f"{BASE}/personas",
headers=HEADERS,
json={
"name": p["name"],
"description": f"[{p['segment']}] {p['title']}\n\n{p['description']}",
"workspace_id": WORKSPACE_ID,
},
).json()
if "error" in resp:
raise Exception(f"Failed: {p['name']}: {resp['error']['message']}")
persona_ids.append(resp["id"])
print(f"✓ Created: {p['name']} ({p['segment']}) — {resp['id']}")
return persona_ids
async function generatePersonaDescriptions(crmAnalysis) {
const PERSONA_SCHEMA = { type: "json_schema", json_schema: {
name: "personas", strict: true,
schema: {
type: "object",
properties: {
personas: {
type: "array",
items: {
type: "object",
properties: {
name: { type: "string" }, segment: { type: "string" },
title: { type: "string" }, description: { type: "string" },
},
required: ["name", "segment", "title", "description"],
},
},
},
required: ["personas"],
},
}};
const resp = await mavera.responses.create({
model: "mavera-1",
input: [{
role: "user",
content:
`Based on this CRM analysis for ${COMPANY.name}, create 5 detailed personas.\n\n` +
`## CRM Analysis\n${JSON.stringify(crmAnalysis, null, 2)}\n\n` +
`## Company\n${JSON.stringify(COMPANY, null, 2)}\n\n` +
"Include title, company context, product usage, and next-year priorities.",
}],
response_format: PERSONA_SCHEMA,
});
return JSON.parse(resp.output[0].content[0].text).personas;
}
async function createPersonasFromDescriptions(personaDescs) {
const ids = [];
for (const p of personaDescs) {
const resp = await fetch(`${BASE}/personas`, {
method: "POST", headers: HEADERS,
body: JSON.stringify({
name: p.name,
description: `[${p.segment}] ${p.title}\n\n${p.description}`,
workspace_id: WORKSPACE_ID,
}),
}).then((r) => r.json());
if (resp.error) throw new Error(`Failed: ${p.name}: ${resp.error.message}`);
ids.push(resp.id);
console.log(`✓ Created: ${p.name} (${p.segment}) — ${resp.id}`);
}
return ids;
}
Stage 3 — Focus Groups on Next Year’s Themes
def run_theme_focus_group(persona_ids: list[str]) -> dict:
"""Run a focus group validating candidate themes for next year."""
themes_block = "\n".join(
f"{i+1}. **{t['theme']}**: {t['description']}"
for i, t in enumerate(CANDIDATE_THEMES)
)
payload = {
"name": f"{PLAN_YEAR} Annual Planning — Theme Validation",
"sample_size": 30,
"persona_ids": persona_ids,
"workspace_id": WORKSPACE_ID,
"questions": [
{
"question": (
f"As a current or potential customer, rank these {PLAN_YEAR} strategic themes "
f"from most important to you (1st) to least (5th):\n\n{themes_block}"
),
"type": "RANKING",
"options": [t["theme"] for t in CANDIDATE_THEMES],
"order": 1,
},
{
"question": (
f"For the theme you ranked #1, rate how much it would increase "
f"your likelihood to invest more in {COMPANY['name']}. (1-10)"
),
"type": "LIKERT",
"scale": 10,
"order": 2,
},
{
"question": (
"Which theme would you be most disappointed if we did NOT pursue? "
"Why?"
),
"type": "OPEN_ENDED",
"order": 3,
},
{
"question": (
f"Is there a strategic direction NOT listed above that you think "
f"{COMPANY['name']} should prioritize in {PLAN_YEAR}?"
),
"type": "OPEN_ENDED",
"order": 4,
},
{
"question": (
f"Rate your overall confidence that {COMPANY['name']} is heading "
f"in the right direction. (0 = no confidence, 10 = very confident)"
),
"type": "NPS",
"order": 5,
},
],
}
resp = requests.post(f"{BASE}/focus-groups", headers=HEADERS, json=payload).json()
if "error" in resp:
raise Exception(resp["error"]["message"])
print(f"✓ Theme focus group launched: {resp['id']}")
return resp
def poll_focus_group(fg_id: str, timeout_min: int = 15) -> dict:
for _ in range(timeout_min * 6):
resp = requests.get(f"{BASE}/focus-groups/{fg_id}", headers=HEADERS).json()
if "error" in resp:
raise Exception(resp["error"]["message"])
if resp.get("status") == "COMPLETED":
print("✓ Theme focus group completed")
return resp
if resp.get("status") == "FAILED":
raise Exception("Focus group failed")
time.sleep(10)
raise TimeoutError("Focus group timed out")
async function runThemeFocusGroup(personaIds) {
const themesBlock = CANDIDATE_THEMES
.map((t, i) => `${i + 1}. **${t.theme}**: ${t.description}`)
.join("\n");
const payload = {
name: `${PLAN_YEAR} Annual Planning — Theme Validation`,
sample_size: 30,
persona_ids: personaIds,
workspace_id: WORKSPACE_ID,
questions: [
{
question: `Rank these ${PLAN_YEAR} strategic themes:\n\n${themesBlock}`,
type: "RANKING",
options: CANDIDATE_THEMES.map((t) => t.theme),
order: 1,
},
{
question: `For your #1 theme, rate how much it increases your likelihood to invest more. (1-10)`,
type: "LIKERT", scale: 10, order: 2,
},
{
question: "Which theme would you be most disappointed if we did NOT pursue? Why?",
type: "OPEN_ENDED", order: 3,
},
{
question: `Any strategic direction not listed that ${COMPANY.name} should prioritize?`,
type: "OPEN_ENDED", order: 4,
},
{
question: `Rate your confidence ${COMPANY.name} is heading in the right direction. (0-10)`,
type: "NPS", order: 5,
},
],
};
const resp = await fetch(`${BASE}/focus-groups`, {
method: "POST", headers: HEADERS,
body: JSON.stringify(payload),
}).then((r) => r.json());
if (resp.error) throw new Error(resp.error.message);
console.log(`✓ Theme focus group launched: ${resp.id}`);
return resp;
}
async function pollFocusGroup(fgId, timeoutMin = 15) {
for (let i = 0; i < timeoutMin * 6; i++) {
const resp = await fetch(`${BASE}/focus-groups/${fgId}`, { headers: HEADERS }).then((r) => r.json());
if (resp.error) throw new Error(resp.error.message);
if (resp.status === "COMPLETED") return resp;
if (resp.status === "FAILED") throw new Error("Focus group failed");
await new Promise((r) => setTimeout(r, 10000));
}
throw new Error("Focus group timed out");
}
Stage 4 — Mave Researches Market Context
def research_market_context() -> dict:
"""Use Mave to research market trends, competitive landscape, and benchmarks."""
research_prompts = [
{
"label": "market_trends",
"message": (
f"Research the top trends in the {COMPANY['category']} market for {PLAN_YEAR}. "
f"Focus on: technology shifts, buyer behavior changes, "
f"emerging competitors, and market size projections. Cite sources."
),
},
{
"label": "competitive_landscape",
"message": (
f"What are {COMPANY['name']}'s main competitors doing for {PLAN_YEAR}? "
f"Any recent funding, product launches, acquisitions, or pivots? "
f"How are they positioning for next year? Cite sources."
),
},
{
"label": "industry_benchmarks",
"message": (
f"What are typical SaaS benchmarks for a company at {COMPANY['current_arr']} ARR "
f"growing at {COMPANY['growth_rate']}? Include: ideal CAC:LTV ratio, "
f"net revenue retention targets, magic number, and marketing spend as % of revenue. "
f"Cite sources."
),
},
]
thread_id = None
research = {}
for prompt in research_prompts:
payload = {"message": prompt["message"]}
if thread_id:
payload["thread_id"] = thread_id
resp = requests.post(
f"{BASE}/mave/chat",
headers=HEADERS,
json=payload,
timeout=120,
).json()
if "error" in resp:
raise Exception(resp["error"]["message"])
if not thread_id:
thread_id = resp.get("thread_id")
research[prompt["label"]] = {
"content": resp.get("content", ""),
"sources": resp.get("sources", []),
}
print(f"✓ Mave: {prompt['label']} ({len(resp.get('content', ''))} chars)")
time.sleep(2)
return {"thread_id": thread_id, "research": research}
async function researchMarketContext() {
const prompts = [
{
label: "market_trends",
message: `Research the top trends in ${COMPANY.category} for ${PLAN_YEAR}. Focus on technology shifts, buyer behavior, emerging competitors.`,
},
{
label: "competitive_landscape",
message: `What are ${COMPANY.name}'s main competitors doing for ${PLAN_YEAR}? Recent funding, launches, acquisitions?`,
},
{
label: "industry_benchmarks",
message: `SaaS benchmarks for a company at ${COMPANY.current_arr} ARR growing at ${COMPANY.growth_rate}? CAC:LTV, NRR, marketing spend %.`,
},
];
let threadId = null;
const research = {};
for (const p of prompts) {
const payload = { message: p.message };
if (threadId) payload.thread_id = threadId;
const resp = await fetch(`${BASE}/mave/chat`, {
method: "POST", headers: HEADERS,
body: JSON.stringify(payload),
signal: AbortSignal.timeout(120000),
}).then((r) => r.json());
if (resp.error) throw new Error(resp.error.message);
if (!threadId) threadId = resp.thread_id;
research[p.label] = { content: resp.content || "", sources: resp.sources || [] };
console.log(`✓ Mave: ${p.label}`);
await new Promise((r) => setTimeout(r, 2000));
}
return { thread_id: threadId, research };
}
Stage 5 — Generate the Annual Plan
PLAN_SECTIONS = [
"executive_summary",
"market_context",
"strategic_themes",
"target_segments",
"channel_strategy",
"budget_allocation",
"okrs_and_kpis",
"quarterly_roadmap",
"risks_and_mitigations",
]
def generate_annual_plan(
crm_analysis: dict, fg_results: dict, market_research: dict
) -> dict:
"""Generate each section of the annual plan using Generate API."""
# Compile all inputs into context
context = (
f"# {PLAN_YEAR} Annual Marketing Plan Inputs\n\n"
f"## Company\n{json.dumps(COMPANY, indent=2)}\n\n"
f"## CRM Analysis\n{json.dumps(crm_analysis, indent=2)}\n\n"
f"## Focus Group Results (Theme Validation)\n"
)
for result in fg_results.get("results", []):
context += f"### {result.get('type')}: {result.get('question', '')[:80]}\n"
if result.get("summary"):
context += f"{result['summary']}\n"
if result.get("ranking_distribution"):
context += f"Rankings: {json.dumps(result['ranking_distribution'])}\n"
if result.get("mean_score"):
context += f"Mean: {result['mean_score']}\n"
context += "\n"
context += "## Market Research\n"
for label, data in market_research["research"].items():
context += f"### {label}\n{data['content'][:1000]}\n\n"
plan_sections = {}
for section in PLAN_SECTIONS:
resp = requests.post(
f"{BASE}/generate",
headers=HEADERS,
json={
"prompt": (
f"Write the '{section.replace('_', ' ').title()}' section of the "
f"{PLAN_YEAR} annual marketing plan for {COMPANY['name']}.\n\n"
f"Use the data and insights below. Be specific — include numbers, "
f"timelines, and owners where appropriate. "
f"Write in a professional tone suitable for a board presentation.\n\n"
f"{context}"
),
"workspace_id": WORKSPACE_ID,
"max_tokens": 2000,
},
).json()
if "error" in resp:
raise Exception(f"Generate failed for {section}: {resp['error']['message']}")
plan_sections[section] = resp.get("content", resp.get("text", ""))
print(f"✓ Generated: {section.replace('_', ' ').title()}")
time.sleep(2)
return plan_sections
const PLAN_SECTIONS = [
"executive_summary", "market_context", "strategic_themes",
"target_segments", "channel_strategy", "budget_allocation",
"okrs_and_kpis", "quarterly_roadmap", "risks_and_mitigations",
];
async function generateAnnualPlan(crmAnalysis, fgResults, marketResearch) {
let context = `# ${PLAN_YEAR} Annual Marketing Plan Inputs\n\n`;
context += `## Company\n${JSON.stringify(COMPANY, null, 2)}\n\n`;
context += `## CRM Analysis\n${JSON.stringify(crmAnalysis, null, 2)}\n\n`;
context += "## Focus Group Results\n";
for (const result of fgResults.results || []) {
context += `### ${result.type}: ${(result.question || "").slice(0, 80)}\n`;
if (result.summary) context += `${result.summary}\n`;
context += "\n";
}
context += "## Market Research\n";
for (const [label, data] of Object.entries(marketResearch.research)) {
context += `### ${label}\n${data.content.slice(0, 1000)}\n\n`;
}
const planSections = {};
for (const section of PLAN_SECTIONS) {
const resp = await fetch(`${BASE}/generate`, {
method: "POST", headers: HEADERS,
body: JSON.stringify({
prompt:
`Write the '${section.replace(/_/g, " ")}' section of the ${PLAN_YEAR} ` +
`annual marketing plan for ${COMPANY.name}.\n\n${context}`,
workspace_id: WORKSPACE_ID,
max_tokens: 2000,
}),
}).then((r) => r.json());
if (resp.error) throw new Error(`Generate failed for ${section}: ${resp.error.message}`);
planSections[section] = resp.content || resp.text || "";
console.log(`✓ Generated: ${section.replace(/_/g, " ")}`);
await new Promise((r) => setTimeout(r, 2000));
}
return planSections;
}
Stage 6 — Speak Validation Conversations
Run live voice conversations with key personas to stress-test the plan.SPEAK_QUESTIONS = [
f"I'm going to share our top strategic theme for {PLAN_YEAR}. Tell me honestly — does this resonate with your needs?",
"What's missing from this plan? What would make you more excited about our direction?",
"If you had to bet, would this plan make you increase your investment with us, keep it the same, or decrease it?",
"What's the one thing we could do next year that would make the biggest difference for you?",
]
def run_speak_conversations(persona_ids: list[str], plan_summary: str) -> list[dict]:
"""Run Speak conversations with each persona to validate the plan."""
conversations = []
for pid in persona_ids[:3]: # Top 3 personas for voice validation
resp = requests.post(
f"{BASE}/speak/conversations",
headers=HEADERS,
json={
"persona_id": pid,
"workspace_id": WORKSPACE_ID,
"context": (
f"You are reviewing the {PLAN_YEAR} annual marketing plan for {COMPANY['name']}. "
f"Here is the plan summary:\n\n{plan_summary}"
),
"initial_message": SPEAK_QUESTIONS[0],
},
).json()
if "error" in resp:
print(f" Warning: Speak failed for persona {pid}: {resp['error']['message']}")
continue
conversation_id = resp.get("id")
conversations.append({
"persona_id": pid,
"conversation_id": conversation_id,
"initial_response": resp.get("response", ""),
})
print(f"✓ Speak conversation started: {conversation_id}")
# Follow-up questions
for question in SPEAK_QUESTIONS[1:]:
time.sleep(3)
follow_up = requests.post(
f"{BASE}/speak/conversations/{conversation_id}/messages",
headers=HEADERS,
json={"message": question},
).json()
if "error" not in follow_up:
conversations[-1][f"response_{question[:30]}"] = follow_up.get("response", "")
time.sleep(2)
print(f"✓ Completed {len(conversations)} Speak conversations")
return conversations
const SPEAK_QUESTIONS = [
`I'm sharing our top strategic theme for ${PLAN_YEAR}. Does this resonate with your needs?`,
"What's missing from this plan? What would make you more excited?",
"Would this plan make you increase, maintain, or decrease your investment with us?",
"What's the one thing we could do next year that would matter most to you?",
];
async function runSpeakConversations(personaIds, planSummary) {
const conversations = [];
for (const pid of personaIds.slice(0, 3)) {
const resp = await fetch(`${BASE}/speak/conversations`, {
method: "POST", headers: HEADERS,
body: JSON.stringify({
persona_id: pid, workspace_id: WORKSPACE_ID,
context:
`Reviewing the ${PLAN_YEAR} annual plan for ${COMPANY.name}.\n\n${planSummary}`,
initial_message: SPEAK_QUESTIONS[0],
}),
}).then((r) => r.json());
if (resp.error) { console.warn(`Speak failed: ${resp.error.message}`); continue; }
const conv = { persona_id: pid, conversation_id: resp.id, initial_response: resp.response || "" };
for (const q of SPEAK_QUESTIONS.slice(1)) {
await new Promise((r) => setTimeout(r, 3000));
const followUp = await fetch(`${BASE}/speak/conversations/${resp.id}/messages`, {
method: "POST", headers: HEADERS,
body: JSON.stringify({ message: q }),
}).then((r) => r.json());
if (!followUp.error) conv[`response_${q.slice(0, 30)}`] = followUp.response || "";
}
conversations.push(conv);
console.log(`✓ Speak conversation: ${resp.id}`);
await new Promise((r) => setTimeout(r, 2000));
}
return conversations;
}
Running the Full Pipeline
def run_annual_planning():
print("=" * 60)
print(f"{PLAN_YEAR} ANNUAL PLANNING KICKOFF")
print("=" * 60)
# Stage 1: CRM Analysis
print("\n--- Stage 1: CRM Data Analysis ---")
crm_analysis = analyze_crm_data(SAMPLE_CRM_DATA)
# Stage 2: Create Personas
print("\n--- Stage 2: Creating Personas from CRM Data ---")
persona_descs = generate_persona_descriptions(crm_analysis)
persona_ids = create_personas_from_descriptions(persona_descs)
# Stage 3: Theme Focus Group
print("\n--- Stage 3: Theme Validation Focus Group ---")
fg = run_theme_focus_group(persona_ids)
fg_results = poll_focus_group(fg["id"])
# Stage 4: Market Research
print("\n--- Stage 4: Mave Market Research ---")
market_research = research_market_context()
# Stage 5: Generate Plan
print("\n--- Stage 5: Generating Annual Plan ---")
plan_sections = generate_annual_plan(crm_analysis, fg_results, market_research)
# Compile plan document
plan_doc = f"# {COMPANY['name']} — {PLAN_YEAR} Annual Marketing Plan\n\n"
for section, content in plan_sections.items():
plan_doc += f"## {section.replace('_', ' ').title()}\n\n{content}\n\n---\n\n"
with open(f"annual_plan_{PLAN_YEAR}.md", "w") as f:
f.write(plan_doc)
print(f"\n✓ Draft plan saved to annual_plan_{PLAN_YEAR}.md")
# Stage 6: Speak Validation
print("\n--- Stage 6: Speak Validation Conversations ---")
plan_summary = plan_sections.get("executive_summary", plan_doc[:2000])
speak_results = run_speak_conversations(persona_ids, plan_summary)
# Save everything
output = {
"crm_analysis": crm_analysis,
"personas": persona_descs,
"persona_ids": persona_ids,
"theme_validation": {
"results": fg_results.get("results", []),
},
"market_research": {
label: data["content"][:500]
for label, data in market_research["research"].items()
},
"plan_sections": list(plan_sections.keys()),
"speak_conversations": len(speak_results),
}
with open(f"planning_data_{PLAN_YEAR}.json", "w") as f:
json.dump(output, f, indent=2)
print(f"\n{'='*60}")
print(f"{PLAN_YEAR} ANNUAL PLAN — COMPLETE")
print(f"{'='*60}")
print(f" Plan document: annual_plan_{PLAN_YEAR}.md")
print(f" Supporting data: planning_data_{PLAN_YEAR}.json")
print(f" Personas created: {len(persona_ids)}")
print(f" Themes validated: {len(CANDIDATE_THEMES)}")
print(f" Plan sections: {len(plan_sections)}")
print(f" Speak conversations: {len(speak_results)}")
return plan_sections
if __name__ == "__main__":
run_annual_planning()
async function runAnnualPlanning() {
console.log(`${PLAN_YEAR} ANNUAL PLANNING KICKOFF`);
// Stage 1
console.log("\n--- Stage 1: CRM Data Analysis ---");
const crmAnalysis = analyzeCrmData(SAMPLE_CRM_DATA);
// Stage 2
console.log("\n--- Stage 2: Creating Personas ---");
const personaDescs = await generatePersonaDescriptions(crmAnalysis);
const personaIds = await createPersonasFromDescriptions(personaDescs);
// Stage 3
console.log("\n--- Stage 3: Theme Validation ---");
const fg = await runThemeFocusGroup(personaIds);
const fgResults = await pollFocusGroup(fg.id);
// Stage 4
console.log("\n--- Stage 4: Market Research ---");
const marketResearch = await researchMarketContext();
// Stage 5
console.log("\n--- Stage 5: Generating Plan ---");
const planSections = await generateAnnualPlan(crmAnalysis, fgResults, marketResearch);
let planDoc = `# ${COMPANY.name} — ${PLAN_YEAR} Annual Marketing Plan\n\n`;
for (const [section, content] of Object.entries(planSections)) {
planDoc += `## ${section.replace(/_/g, " ")}\n\n${content}\n\n---\n\n`;
}
fs.writeFileSync(`annual_plan_${PLAN_YEAR}.md`, planDoc);
// Stage 6
console.log("\n--- Stage 6: Speak Validation ---");
const summary = planSections.executive_summary || planDoc.slice(0, 2000);
const speakResults = await runSpeakConversations(personaIds, summary);
console.log(`\n${PLAN_YEAR} ANNUAL PLAN — COMPLETE`);
console.log(` Plan: annual_plan_${PLAN_YEAR}.md`);
console.log(` Sections: ${Object.keys(planSections).length}`);
console.log(` Speak conversations: ${speakResults.length}`);
return planSections;
}
runAnnualPlanning();
Example Output
The pipeline produces a full plan document. Here’s a sample executive summary section:## Executive Summary
Acme's 2027 marketing plan is built on three validated strategic themes,
persona-validated by 5 customer segments representing 280 active accounts.
**Theme Priority (Focus Group Ranking, N=30):**
1. Integration Ecosystem (ranked #1 by 40% of respondents)
2. Product-Led Growth (ranked #1 by 27%)
3. Content & Thought Leadership (ranked #1 by 20%)
**Key Metrics Targets:**
- ARR: $4.2M → $9.5M (126% growth)
- Customers: 280 → 600
- Net Revenue Retention: 115% → 130%
- CAC Payback: 12 months → 9 months
**Budget Allocation:**
- Product-Led Growth: 35% ($1.2M)
- Integration Ecosystem: 25% ($875K)
- Content & Thought Leadership: 20% ($700K)
- Enterprise Expansion: 15% ($525K)
- International: 5% ($175K)
**Validated by:** 5 CRM-derived personas across enterprise, mid-market,
agency, startup, and churned segments. 3 Speak conversations confirmed
integration ecosystem as the top priority across all segments.
Variations
Multiple scenarios
Multiple scenarios
Run the pipeline 3 times with different theme priorities (aggressive, balanced, conservative):
scenarios = {
"aggressive": {"budget_multiplier": 1.5, "growth_target": "150%"},
"balanced": {"budget_multiplier": 1.0, "growth_target": "100%"},
"conservative": {"budget_multiplier": 0.7, "growth_target": "60%"},
}
for name, params in scenarios.items():
COMPANY["growth_target"] = params["growth_target"]
plan = run_annual_planning()
Real CRM data instead of sample
Real CRM data instead of sample
Replace
SAMPLE_CRM_DATA with a real CSV export:def load_crm_csv(path: str) -> list[dict]:
with open(path) as f:
return list(csv.DictReader(f))
crm_data = load_crm_csv("crm_export.csv")
crm_analysis = analyze_crm_data(crm_data)
Skip Speak for faster execution
Skip Speak for faster execution
If you don’t need voice validation, skip Stage 6. The plan is still validated by Focus Groups and market research:
# Comment out Stage 6
# speak_results = run_speak_conversations(persona_ids, plan_summary)
Department-specific plans
Department-specific plans
After the company-level plan, generate department-specific versions:
departments = ["demand_gen", "product_marketing", "content", "brand"]
for dept in departments:
resp = requests.post(f"{BASE}/generate", headers=HEADERS, json={
"prompt": f"Extract the {dept} team's plan from the company plan: {plan_doc[:3000]}",
"workspace_id": WORKSPACE_ID,
})
Credits Estimate
| Stage | Typical Cost | Notes |
|---|---|---|
| CRM analysis (local) | 0 credits | Runs locally |
| Generate persona descriptions (1 chat) | 5–15 credits | |
| Create 5 personas | 0 | Persona creation is free |
| Focus Group (N=30, 5 questions) | 150–300 credits | Theme validation |
| Mave research (3 turns) | 30–90 credits | Market context |
| Generate plan (9 sections) | 90–180 credits | One Generate call per section |
| Speak conversations (3 personas) | 30–90 credits | Optional voice validation |
| Total (full pipeline) | ~305–675 credits | |
| Total (without Speak) | ~275–585 credits |
This is the most credit-intensive playbook. Run a quick version first (N=15 focus group, 3 plan sections, no Speak) to validate the pipeline before committing to the full run.
See Also
Market Entry Research
Deep market research for new opportunities
Positioning Workshop
Validate positioning as part of planning
Pricing Research
Test pricing alongside strategic planning
Brand Perception Audit
Measure brand health before planning
News-Triggered Research
Monitor market changes during plan execution
Focus Groups
Question types and configuration