Mavera Surfaces Used
| Surface | Role |
|---|---|
Personas (POST /personas, GET /personas) | Create and retrieve 5 ICP-specific personas |
Focus Groups (POST /focus-groups) | Run positioning test with Ranking + Semantic Differential + Open-Ended questions |
Chat + response_format | Synthesize focus group results into a final positioning recommendation |
Traditional positioning workshops take days and cost $10K+. This playbook produces persona-validated positioning in under an hour with no external tooling — just Mavera’s Personas and Focus Groups APIs.
What Value Does Mavera Add?
| Value | How |
|---|---|
| Insurance | Test positioning with 5 distinct ICP segments before committing. Catch blind spots that internal teams miss. |
| Opening new doors | Run positioning sprints weekly during product pivots — cost and speed make rapid iteration practical. |
| Saving time | A full positioning workshop (persona creation → focus group → recommendation) runs in ~30 minutes instead of 2–3 days. |
When to Use This
- You’re defining or refreshing your positioning statement and want audience validation before committing.
- You have 2–5 candidate positioning statements and need to pick a winner — fast.
- You want to test how different ICP segments interpret the same positioning.
- You’re preparing for a rebrand, product launch, or funding round and need data-backed positioning.
What You Need
| Requirement | Details |
|---|---|
| Mavera API key | Starts with mvra_live_. Get one at Developer Settings. |
| Workspace ID | From your dashboard URL (ws_...). |
| 3 positioning statements | Candidate statements to test (follow the template: For [audience], [product] is the [category] that [differentiator]). |
| ICP definition | Enough detail to create 5 representative personas (title, industry, pain points, budget authority). |
| Credits | ~200–500 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
The Workshop Framework
Question Types
| Question | Type | What It Measures |
|---|---|---|
| ”Rank these 3 positioning statements from most to least compelling” | Ranking | Overall preference order across all personas |
| ”Rate Statement A on: Generic ←→ Differentiated” | Semantic Differential | Perception on a bipolar scale |
| ”Rate Statement A on: Confusing ←→ Clear” | Semantic Differential | Comprehension |
| ”Rate Statement A on: Forgettable ←→ Memorable” | Semantic Differential | Stickiness |
| ”What would make the top-ranked statement stronger?” | Open-Ended | Qualitative improvement suggestions |
The Flow
1
Define your 3 positioning statements
Use the standard template: For [audience], [product] is the [category] that [differentiator]. Having exactly 3 keeps the ranking question manageable for respondents.
2
Create 5 ICP personas
Build personas representing distinct segments of your ideal customer profile. Vary by seniority, function, company size, and pain point so each perspective is unique.
3
Run the Focus Group
30 respondents across the 5 personas (6 per persona). Questions: 1 Ranking, 3 Semantic Differentials, 1 Open-Ended.
4
Poll for completion
Focus Groups with 30 respondents and 5 questions typically complete in 3-8 minutes.
5
Analyze results
Extract ranking distribution, semantic differential averages, and open-ended themes.
6
Synthesize recommendation
Use Chat with structured output to produce a final positioning recommendation with segment-level detail.
Code: Full Positioning Workshop
Setup and Positioning Statements
import os
import json
import time
import requests
from openai import OpenAI
MAVERA_API_KEY = os.environ["MAVERA_API_KEY"]
WORKSPACE_ID = os.environ["MAVERA_WORKSPACE_ID"]
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)
POSITIONING_STATEMENTS = [
{
"label": "A",
"statement": (
"For growth-stage B2B companies, Acme is the market intelligence platform "
"that replaces guesswork with persona-validated data — so you ship messaging "
"that resonates on the first try."
),
},
{
"label": "B",
"statement": (
"For marketing leaders, Acme is the AI research assistant that simulates "
"your target audience — giving you focus group insights in minutes, "
"not months."
),
},
{
"label": "C",
"statement": (
"For product marketers, Acme is the positioning engine that tests your "
"messaging against synthetic buyers — so every campaign launches with "
"confidence backed by data."
),
},
]
ICP_PERSONAS = [
{
"name": "VP of Marketing — Growth-Stage SaaS",
"description": (
"VP Marketing at a Series B SaaS company (50-200 employees). "
"Owns positioning, messaging, and demand gen. Budget: $500K-$2M/year. "
"Pain: can't afford agency research but needs data-backed messaging. "
"Evaluates tools on speed-to-insight and ROI."
),
},
{
"name": "Head of Product Marketing — Enterprise",
"description": (
"Head of PMM at a $50M+ ARR enterprise software company. "
"Manages positioning for multiple product lines. "
"Pain: positioning workshops take weeks and results are subjective. "
"Needs quantitative validation for executive buy-in."
),
},
{
"name": "CMO — Mid-Market B2B",
"description": (
"CMO at a mid-market B2B company (200-1000 employees). "
"Reports to CEO, presents to board. Budget: $2M-$10M/year. "
"Pain: marketing is perceived as a cost center, needs measurable impact. "
"Values tools that produce board-ready outputs."
),
},
{
"name": "Content Marketing Manager",
"description": (
"Content marketing manager at a B2B startup. "
"Produces blog posts, whitepapers, and social content daily. "
"Pain: writes copy without knowing if it resonates with the buyer. "
"Wants fast feedback loops, not quarterly brand studies."
),
},
{
"name": "Founder / CEO — Early Stage",
"description": (
"Technical founder at a pre-Series A startup (5-20 employees). "
"Wears the marketing hat. Budget: $50K-$200K/year total. "
"Pain: can't hire a brand strategist yet but needs sharp positioning "
"for investor decks and landing pages. Values simplicity and speed."
),
},
]
import OpenAI from "openai";
const MAVERA_API_KEY = process.env.MAVERA_API_KEY;
const WORKSPACE_ID = process.env.MAVERA_WORKSPACE_ID;
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 POSITIONING_STATEMENTS = [
{
label: "A",
statement:
"For growth-stage B2B companies, Acme is the market intelligence platform " +
"that replaces guesswork with persona-validated data — so you ship messaging " +
"that resonates on the first try.",
},
{
label: "B",
statement:
"For marketing leaders, Acme is the AI research assistant that simulates " +
"your target audience — giving you focus group insights in minutes, not months.",
},
{
label: "C",
statement:
"For product marketers, Acme is the positioning engine that tests your " +
"messaging against synthetic buyers — so every campaign launches with " +
"confidence backed by data.",
},
];
const ICP_PERSONAS = [
{
name: "VP of Marketing — Growth-Stage SaaS",
description:
"VP Marketing at a Series B SaaS company (50-200 employees). " +
"Owns positioning, messaging, and demand gen. Budget: $500K-$2M/year. " +
"Pain: can't afford agency research but needs data-backed messaging.",
},
{
name: "Head of Product Marketing — Enterprise",
description:
"Head of PMM at a $50M+ ARR enterprise software company. " +
"Manages positioning for multiple product lines. " +
"Pain: positioning workshops take weeks and results are subjective.",
},
{
name: "CMO — Mid-Market B2B",
description:
"CMO at a mid-market B2B company (200-1000 employees). " +
"Reports to CEO, presents to board. Budget: $2M-$10M/year. " +
"Values tools that produce board-ready outputs.",
},
{
name: "Content Marketing Manager",
description:
"Content marketing manager at a B2B startup. " +
"Produces blog posts, whitepapers, and social content daily. " +
"Wants fast feedback loops, not quarterly brand studies.",
},
{
name: "Founder / CEO — Early Stage",
description:
"Technical founder at a pre-Series A startup (5-20 employees). " +
"Wears the marketing hat. Budget: $50K-$200K/year total. " +
"Values simplicity and speed.",
},
];
Stage 1 — Create Personas
def create_personas() -> list[str]:
"""Create 5 ICP personas and return their IDs."""
persona_ids = []
for persona in ICP_PERSONAS:
resp = requests.post(
f"{BASE}/personas",
headers=HEADERS,
json={
"name": persona["name"],
"description": persona["description"],
"workspace_id": WORKSPACE_ID,
},
).json()
if "error" in resp:
raise Exception(f"Failed to create persona '{persona['name']}': {resp['error']['message']}")
persona_ids.append(resp["id"])
print(f"✓ Created persona: {persona['name']} ({resp['id']})")
return persona_ids
async function createPersonas() {
const personaIds = [];
for (const persona of ICP_PERSONAS) {
const resp = await fetch(`${BASE}/personas`, {
method: "POST",
headers: HEADERS,
body: JSON.stringify({
name: persona.name,
description: persona.description,
workspace_id: WORKSPACE_ID,
}),
}).then((r) => r.json());
if (resp.error)
throw new Error(`Failed to create persona '${persona.name}': ${resp.error.message}`);
personaIds.push(resp.id);
console.log(`✓ Created persona: ${persona.name} (${resp.id})`);
}
return personaIds;
}
If you already have personas from a previous session, skip creation and pass their IDs directly. Use
GET /personas to list existing ones.Stage 2 — Run the Focus Group
The focus group uses 5 questions: 1 Ranking, 3 Semantic Differentials (one per statement), and 1 Open-Ended.def build_statements_block() -> str:
"""Format positioning statements for inclusion in questions."""
return "\n".join(
f"Statement {s['label']}: \"{s['statement']}\""
for s in POSITIONING_STATEMENTS
)
def run_positioning_focus_group(persona_ids: list[str]) -> dict:
"""Run the positioning workshop focus group."""
statements_block = build_statements_block()
payload = {
"name": "Positioning Workshop — 3 Statements × 5 Personas",
"sample_size": 30,
"persona_ids": persona_ids,
"workspace_id": WORKSPACE_ID,
"questions": [
{
"question": (
"Read these 3 positioning statements carefully, then rank them "
"from most compelling (1st) to least compelling (3rd).\n\n"
f"{statements_block}"
),
"type": "RANKING",
"options": [
f"Statement {s['label']}" for s in POSITIONING_STATEMENTS
],
"order": 1,
},
{
"question": (
f"Rate Statement A on this scale:\n\n"
f"\"{POSITIONING_STATEMENTS[0]['statement']}\""
),
"type": "SEMANTIC_DIFFERENTIAL",
"left_anchor": "Generic / could be anyone",
"right_anchor": "Differentiated / clearly unique",
"scale": 7,
"order": 2,
},
{
"question": (
f"Rate Statement B on this scale:\n\n"
f"\"{POSITIONING_STATEMENTS[1]['statement']}\""
),
"type": "SEMANTIC_DIFFERENTIAL",
"left_anchor": "Confusing / hard to understand",
"right_anchor": "Clear / instantly understood",
"scale": 7,
"order": 3,
},
{
"question": (
f"Rate Statement C on this scale:\n\n"
f"\"{POSITIONING_STATEMENTS[2]['statement']}\""
),
"type": "SEMANTIC_DIFFERENTIAL",
"left_anchor": "Forgettable / bland",
"right_anchor": "Memorable / would stick with me",
"scale": 7,
"order": 4,
},
{
"question": (
"You ranked the statements above. For the one you ranked #1:\n"
"- What specifically makes it compelling?\n"
"- What would make it even stronger?\n"
"- Is there anything misleading or unclear?"
),
"type": "OPEN_ENDED",
"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"✓ Focus group created: {resp['id']}")
print(f" Sample size: {payload['sample_size']}")
print(f" Personas: {len(persona_ids)}")
print(f" Questions: {len(payload['questions'])}")
return resp
def poll_focus_group(fg_id: str, timeout_min: int = 15) -> dict:
"""Poll until the focus group completes."""
start = time.time()
for attempt 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"])
status = resp.get("status", "UNKNOWN")
elapsed = int(time.time() - start)
if status == "COMPLETED":
print(f"✓ Focus group completed in {elapsed}s")
return resp
if status == "FAILED":
raise Exception(f"Focus group failed: {resp.get('error', 'Unknown error')}")
print(f" Polling... status={status} ({elapsed}s elapsed)")
time.sleep(10)
raise TimeoutError(f"Focus group {fg_id} did not complete in {timeout_min} min")
function buildStatementsBlock() {
return POSITIONING_STATEMENTS.map(
(s) => `Statement ${s.label}: "${s.statement}"`
).join("\n");
}
async function runPositioningFocusGroup(personaIds) {
const statementsBlock = buildStatementsBlock();
const payload = {
name: "Positioning Workshop — 3 Statements × 5 Personas",
sample_size: 30,
persona_ids: personaIds,
workspace_id: WORKSPACE_ID,
questions: [
{
question:
"Read these 3 positioning statements carefully, then rank them " +
"from most compelling (1st) to least compelling (3rd).\n\n" +
statementsBlock,
type: "RANKING",
options: POSITIONING_STATEMENTS.map((s) => `Statement ${s.label}`),
order: 1,
},
{
question: `Rate Statement A on this scale:\n\n"${POSITIONING_STATEMENTS[0].statement}"`,
type: "SEMANTIC_DIFFERENTIAL",
left_anchor: "Generic / could be anyone",
right_anchor: "Differentiated / clearly unique",
scale: 7,
order: 2,
},
{
question: `Rate Statement B on this scale:\n\n"${POSITIONING_STATEMENTS[1].statement}"`,
type: "SEMANTIC_DIFFERENTIAL",
left_anchor: "Confusing / hard to understand",
right_anchor: "Clear / instantly understood",
scale: 7,
order: 3,
},
{
question: `Rate Statement C on this scale:\n\n"${POSITIONING_STATEMENTS[2].statement}"`,
type: "SEMANTIC_DIFFERENTIAL",
left_anchor: "Forgettable / bland",
right_anchor: "Memorable / would stick with me",
scale: 7,
order: 4,
},
{
question:
"You ranked the statements above. For the one you ranked #1:\n" +
"- What specifically makes it compelling?\n" +
"- What would make it even stronger?\n" +
"- Is there anything misleading or unclear?",
type: "OPEN_ENDED",
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(`✓ Focus group created: ${resp.id}`);
return resp;
}
async function pollFocusGroup(fgId, timeoutMin = 15) {
const start = Date.now();
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") {
console.log(`✓ Focus group completed in ${Math.round((Date.now() - start) / 1000)}s`);
return resp;
}
if (resp.status === "FAILED")
throw new Error(`Focus group failed: ${resp.error || "Unknown"}`);
await new Promise((r) => setTimeout(r, 10000));
}
throw new Error(`Focus group ${fgId} timed out`);
}
Stage 3 — Analyze Results
Extract ranking distributions, semantic differential averages, and open-ended themes.def analyze_results(fg_results: dict) -> dict:
"""Parse focus group results into a structured analysis."""
analysis = {
"ranking": {},
"semantic_differentials": {},
"open_ended_themes": [],
"per_persona": {},
}
for result in fg_results.get("results", []):
q_type = result.get("type")
question = result.get("question", "")
if q_type == "RANKING":
analysis["ranking"] = {
"distribution": result.get("ranking_distribution", {}),
"summary": result.get("summary", ""),
}
elif q_type == "SEMANTIC_DIFFERENTIAL":
label = "Unknown"
for s in POSITIONING_STATEMENTS:
if f"Statement {s['label']}" in question:
label = s["label"]
break
analysis["semantic_differentials"][label] = {
"mean": result.get("mean_score", 0),
"left_anchor": result.get("left_anchor", ""),
"right_anchor": result.get("right_anchor", ""),
"distribution": result.get("score_distribution", {}),
"summary": result.get("summary", ""),
}
elif q_type == "OPEN_ENDED":
analysis["open_ended_themes"] = result.get("themes", [])
analysis["open_ended_responses"] = result.get("responses", [])
# Per-persona breakdown (if available)
for result in fg_results.get("results", []):
for persona_result in result.get("per_persona", []):
pid = persona_result.get("persona_id", "unknown")
if pid not in analysis["per_persona"]:
analysis["per_persona"][pid] = {}
analysis["per_persona"][pid][result.get("question", "")[:50]] = persona_result
return analysis
def print_analysis(analysis: dict):
"""Print a human-readable summary."""
print("\n" + "=" * 60)
print("POSITIONING WORKSHOP RESULTS")
print("=" * 60)
print("\n--- Ranking Distribution ---")
ranking = analysis["ranking"]
if ranking.get("distribution"):
for option, counts in ranking["distribution"].items():
print(f" {option}: {counts}")
if ranking.get("summary"):
print(f" Summary: {ranking['summary']}")
print("\n--- Semantic Differentials ---")
for label, data in analysis["semantic_differentials"].items():
print(f" Statement {label}: {data['mean']:.1f}/7 "
f"({data['left_anchor']} ←→ {data['right_anchor']})")
print("\n--- Open-Ended Themes ---")
for theme in analysis.get("open_ended_themes", []):
print(f" • {theme}")
function analyzeResults(fgResults) {
const analysis = {
ranking: {},
semantic_differentials: {},
open_ended_themes: [],
per_persona: {},
};
for (const result of fgResults.results || []) {
if (result.type === "RANKING") {
analysis.ranking = {
distribution: result.ranking_distribution || {},
summary: result.summary || "",
};
} else if (result.type === "SEMANTIC_DIFFERENTIAL") {
let label = "Unknown";
for (const s of POSITIONING_STATEMENTS) {
if (result.question?.includes(`Statement ${s.label}`)) {
label = s.label;
break;
}
}
analysis.semantic_differentials[label] = {
mean: result.mean_score || 0,
left_anchor: result.left_anchor || "",
right_anchor: result.right_anchor || "",
distribution: result.score_distribution || {},
summary: result.summary || "",
};
} else if (result.type === "OPEN_ENDED") {
analysis.open_ended_themes = result.themes || [];
}
}
return analysis;
}
function printAnalysis(analysis) {
console.log("\n--- Ranking Distribution ---");
for (const [option, counts] of Object.entries(analysis.ranking.distribution || {})) {
console.log(` ${option}: ${JSON.stringify(counts)}`);
}
console.log("\n--- Semantic Differentials ---");
for (const [label, data] of Object.entries(analysis.semantic_differentials)) {
console.log(` Statement ${label}: ${data.mean.toFixed(1)}/7`);
}
console.log("\n--- Open-Ended Themes ---");
for (const theme of analysis.open_ended_themes) {
console.log(` • ${theme}`);
}
}
Stage 4 — Synthesize Recommendation
RECOMMENDATION_SCHEMA = {"type": "json_schema", "json_schema": {
"name": "positioning_recommendation", "strict": True,
"schema": {
"type": "object",
"properties": {
"winning_statement": {"type": "string", "description": "A, B, or C"},
"confidence": {"type": "string", "description": "High, Medium, or Low"},
"ranking_summary": {"type": "string"},
"differentiation_winner": {"type": "string", "description": "Which statement scored highest on differentiation"},
"clarity_winner": {"type": "string", "description": "Which statement scored highest on clarity"},
"memorability_winner": {"type": "string", "description": "Which statement scored highest on memorability"},
"segment_insights": {
"type": "array",
"items": {
"type": "object",
"properties": {
"segment": {"type": "string"},
"preferred_statement": {"type": "string"},
"reasoning": {"type": "string"},
},
"required": ["segment", "preferred_statement", "reasoning"],
},
},
"revision_suggestions": {
"type": "array",
"items": {"type": "string"},
"description": "Specific edits to strengthen the winning statement",
},
"final_recommendation": {"type": "string", "description": "2-3 sentence final recommendation"},
},
"required": [
"winning_statement", "confidence", "ranking_summary",
"differentiation_winner", "clarity_winner", "memorability_winner",
"segment_insights", "revision_suggestions", "final_recommendation",
],
},
}}
def synthesize_recommendation(analysis: dict) -> dict:
"""Use Chat to produce a structured positioning recommendation."""
prompt = (
"You are a positioning strategist. Analyze these focus group results "
"and produce a positioning recommendation.\n\n"
"## Positioning Statements Tested\n"
)
for s in POSITIONING_STATEMENTS:
prompt += f"Statement {s['label']}: \"{s['statement']}\"\n"
prompt += f"\n## Ranking Results\n{json.dumps(analysis['ranking'], indent=2)}\n"
prompt += f"\n## Semantic Differential Scores\n{json.dumps(analysis['semantic_differentials'], indent=2)}\n"
prompt += f"\n## Open-Ended Themes\n{json.dumps(analysis.get('open_ended_themes', []), indent=2)}\n"
prompt += "\nProduce a recommendation with segment-level insights and specific revision suggestions."
resp = mavera.responses.create(
model="mavera-1",
input=[{"role": "user", "content": prompt}],
extra_body={"response_format": RECOMMENDATION_SCHEMA},
)
return json.loads(resp.output[0].content[0].text)
const RECOMMENDATION_SCHEMA = { type: "json_schema", json_schema: {
name: "positioning_recommendation", strict: true,
schema: {
type: "object",
properties: {
winning_statement: { type: "string" },
confidence: { type: "string" },
ranking_summary: { type: "string" },
differentiation_winner: { type: "string" },
clarity_winner: { type: "string" },
memorability_winner: { type: "string" },
segment_insights: {
type: "array",
items: {
type: "object",
properties: {
segment: { type: "string" },
preferred_statement: { type: "string" },
reasoning: { type: "string" },
},
required: ["segment", "preferred_statement", "reasoning"],
},
},
revision_suggestions: { type: "array", items: { type: "string" } },
final_recommendation: { type: "string" },
},
required: [
"winning_statement", "confidence", "ranking_summary",
"differentiation_winner", "clarity_winner", "memorability_winner",
"segment_insights", "revision_suggestions", "final_recommendation",
],
},
}};
async function synthesizeRecommendation(analysis) {
let prompt =
"You are a positioning strategist. Analyze these focus group results " +
"and produce a positioning recommendation.\n\n" +
"## Positioning Statements Tested\n";
for (const s of POSITIONING_STATEMENTS) {
prompt += `Statement ${s.label}: "${s.statement}"\n`;
}
prompt += `\n## Ranking Results\n${JSON.stringify(analysis.ranking, null, 2)}\n`;
prompt += `\n## Semantic Differential Scores\n${JSON.stringify(analysis.semantic_differentials, null, 2)}\n`;
prompt += `\n## Open-Ended Themes\n${JSON.stringify(analysis.open_ended_themes || [], null, 2)}\n`;
prompt += "\nProduce a recommendation with segment-level insights and specific revision suggestions.";
const resp = await mavera.responses.create({
model: "mavera-1",
input: [{ role: "user", content: prompt }],
response_format: RECOMMENDATION_SCHEMA,
});
return JSON.parse(resp.output[0].content[0].text);
}
Running the Full Workshop
def run_workshop():
print("=" * 60)
print("POSITIONING WORKSHOP")
print("=" * 60)
# Stage 1: Create personas
print("\n--- Stage 1: Creating ICP Personas ---")
persona_ids = create_personas()
# Stage 2: Run focus group
print("\n--- Stage 2: Running Focus Group ---")
fg = run_positioning_focus_group(persona_ids)
fg_results = poll_focus_group(fg["id"])
# Stage 3: Analyze
print("\n--- Stage 3: Analyzing Results ---")
analysis = analyze_results(fg_results)
print_analysis(analysis)
# Stage 4: Synthesize
print("\n--- Stage 4: Synthesizing Recommendation ---")
recommendation = synthesize_recommendation(analysis)
print(f"\nWinner: Statement {recommendation['winning_statement']}")
print(f"Confidence: {recommendation['confidence']}")
print(f"\n{recommendation['final_recommendation']}")
if recommendation.get("revision_suggestions"):
print("\nRevision suggestions:")
for s in recommendation["revision_suggestions"]:
print(f" • {s}")
# Save outputs
with open("positioning_results.json", "w") as f:
json.dump({
"analysis": analysis,
"recommendation": recommendation,
"statements": POSITIONING_STATEMENTS,
}, f, indent=2)
print("\n✓ Saved positioning_results.json")
return recommendation
if __name__ == "__main__":
run_workshop()
import fs from "fs";
async function runWorkshop() {
console.log("POSITIONING WORKSHOP");
// Stage 1: Create personas
console.log("\n--- Stage 1: Creating ICP Personas ---");
const personaIds = await createPersonas();
// Stage 2: Run focus group
console.log("\n--- Stage 2: Running Focus Group ---");
const fg = await runPositioningFocusGroup(personaIds);
const fgResults = await pollFocusGroup(fg.id);
// Stage 3: Analyze
console.log("\n--- Stage 3: Analyzing Results ---");
const analysis = analyzeResults(fgResults);
printAnalysis(analysis);
// Stage 4: Synthesize
console.log("\n--- Stage 4: Synthesizing Recommendation ---");
const recommendation = await synthesizeRecommendation(analysis);
console.log(`\nWinner: Statement ${recommendation.winning_statement}`);
console.log(`Confidence: ${recommendation.confidence}`);
console.log(`\n${recommendation.final_recommendation}`);
fs.writeFileSync(
"positioning_results.json",
JSON.stringify({ analysis, recommendation, statements: POSITIONING_STATEMENTS }, null, 2)
);
console.log("\n✓ Saved positioning_results.json");
return recommendation;
}
runWorkshop();
Example Output
{
"winning_statement": "A",
"confidence": "High",
"ranking_summary": "Statement A ranked #1 by 19 of 30 respondents. Statement B ranked #2 by most, with Statement C trailing. VP-level personas strongly preferred A; content marketers leaned toward B.",
"differentiation_winner": "A",
"clarity_winner": "B",
"memorability_winner": "A",
"segment_insights": [
{
"segment": "VP of Marketing",
"preferred_statement": "A",
"reasoning": "Resonated with 'replaces guesswork with data' — directly addresses their accountability pressure."
},
{
"segment": "Content Marketing Manager",
"preferred_statement": "B",
"reasoning": "Preferred the speed emphasis ('minutes, not months') — maps to their daily workflow pain."
},
{
"segment": "Founder / CEO",
"preferred_statement": "A",
"reasoning": "Valued 'resonates on the first try' — limited budget means they can't afford messaging misses."
}
],
"revision_suggestions": [
"Add a speed element from Statement B ('in minutes') to Statement A's value prop",
"Replace 'market intelligence platform' with a more specific category name",
"Test whether 'persona-validated data' is understood by less technical buyers"
],
"final_recommendation": "Ship Statement A as your primary positioning, with the speed language from B incorporated. Statement A wins on differentiation and memorability across 4 of 5 segments. Run a follow-up test after incorporating the revision suggestions."
}
Variations
Test more than 3 statements
Test more than 3 statements
For 4–6 statements, split into two focus groups of 3. Head-to-head the winners in a final round:
# Round 1: Statements A-C
fg1 = run_positioning_focus_group(persona_ids) # with statements A, B, C
# Round 2: Statements D-F
fg2 = run_positioning_focus_group(persona_ids) # with statements D, E, F
# Final: Winners from each round
fg_final = run_positioning_focus_group(persona_ids) # winner of fg1 vs winner of fg2
Add competitive positioning context
Add competitive positioning context
Include competitor positioning in the open-ended question so personas can compare:
{
"question": (
"Here's how 3 competitors position themselves:\n"
f"- Competitor 1: '{comp1_positioning}'\n"
f"- Competitor 2: '{comp2_positioning}'\n\n"
"Does Statement A stand out against these? What's missing?"
),
"type": "OPEN_ENDED",
"order": 6,
}
Weighted persona scoring
Weighted persona scoring
If your VP persona matters 3× more than a Content Manager, weight the ranking:
WEIGHTS = {
vp_marketing_id: 3.0,
head_pmm_id: 2.0,
cmo_id: 3.0,
content_manager_id: 1.0,
founder_id: 2.0,
}
Iterate: refine and re-test
Iterate: refine and re-test
After getting revision suggestions, update the winning statement and run another workshop:Track scores across rounds to see the improvement trajectory.
POSITIONING_STATEMENTS[0]["statement"] = revised_statement_a
# Re-run with same personas
fg2 = run_positioning_focus_group(persona_ids)
Credits Estimate
| Stage | Typical Cost | Notes |
|---|---|---|
| Create 5 personas | 0 | Persona creation is free |
| Focus Group (N=30, 5 questions) | 150–300 credits | Sample size × question count |
| Synthesize recommendation (1 chat call) | 5–15 credits | Single structured output |
| Total | ~155–315 credits |
Start with N=15 (3 per persona) for a quick directional read at half the cost. Scale to N=30+ for statistically meaningful results you’d put in a board deck.
See Also
Focus Groups
All 12 question types including Ranking and Semantic Differential
Personas
Create, list, and manage personas
Market Entry Research
Research the market before testing positioning
Pricing Research
Test pricing alongside positioning
Brand Perception Audit
Measure how your brand is currently perceived
Message Testing Matrix
5 messages × 5 personas for granular message testing