Scenario
Your#product-feedback or #feature-requests channel captures what users actually want — in their own words. This job extracts feature themes from the channel, creates personas representing different user segments, then runs a Mavera Focus Group to pressure-test whether the themes resonate broadly or only with a vocal minority.
Flow: Slack conversations.history (#feedback) → Extract themes via Mave → POST /personas → POST /focus-groups → Validated priorities
Code
import os, requests, time
SL_TOKEN = os.environ["SLACK_BOT_TOKEN"]
SL_BASE = "https://slack.com/api"
SL_H = {"Authorization": f"Bearer {SL_TOKEN}"}
MV = os.environ["MAVERA_API_KEY"]
MV_BASE = "https://app.mavera.io/api/v1"
MV_H = {"Authorization": f"Bearer {MV}", "Content-Type": "application/json"}
CHANNEL_ID = "C0123FEEDBACK"
DAYS_BACK = 60
# 1. Fetch feedback messages
oldest = str(int(time.time()) - DAYS_BACK * 86400)
messages = []
cursor = None
while True:
params = {"channel": CHANNEL_ID, "limit": 200, "oldest": oldest}
if cursor:
params["cursor"] = cursor
r = requests.get(f"{SL_BASE}/conversations.history", headers=SL_H, params=params)
data = r.json()
if not data.get("ok"):
break
messages.extend(data.get("messages", []))
cursor = data.get("response_metadata", {}).get("next_cursor")
if not cursor:
break
time.sleep(1)
feedback = [m for m in messages if m.get("type") == "message"
and not m.get("bot_id") and len(m.get("text","")) > 15]
print(f"Feedback input: {len(feedback)}")
# 2. Extract themes via Mave
corpus = "\n".join(f"- {m.get('text','')[:300]}" for m in feedback[-60:])
themes = requests.post(f"{MV_BASE}/mave/chat", headers=MV_H, json={
"message": f"Product analyst. Extract the top 6 feature themes from {len(feedback)} feedback messages.\n\n"
f"{corpus[:6000]}\n\n"
"For each theme: Name (3-5 words), frequency (count of mentions), "
"representative quote, user impact (1-10).\n"
"Return as a numbered list."
}).json()
themes_text = themes.get("content", "")
print(f"\nThemes:\n{themes_text[:1000]}")
# 3. Create user-segment personas
SEGMENTS = [
{"name": "Power User (Daily)", "desc": "Uses the product 4+ hours/day. Has workarounds for missing features. Values efficiency and keyboard shortcuts."},
{"name": "Casual User (Weekly)", "desc": "Uses the product 2-3 times per week. Needs simplicity. Gets frustrated by complexity. Values intuitive UX."},
{"name": "Team Admin", "desc": "Manages 20+ team members on the platform. Cares about permissions, reporting, onboarding new users. Values admin controls."},
{"name": "New User (< 30 days)", "desc": "Just started using the product. Still learning. Compares to previous tools. Values onboarding and documentation."},
]
persona_ids = []
for seg in SEGMENTS:
p = requests.post(f"{MV_BASE}/personas", headers=MV_H, json={
"name": f"Feedback: {seg['name']}", "description": seg["desc"],
}).json()
persona_ids.append(p["id"])
time.sleep(0.3)
# 4. Focus Group to validate themes
fg = requests.post(f"{MV_BASE}/focus-groups", headers=MV_H, json={
"name": "Product Feedback Validation",
"persona_ids": persona_ids,
"questions": [
f"Here are the top feature requests from our users:\n{themes_text[:1500]}\n\nWhich of these would make the biggest difference for YOU?",
"Is there a feature NOT on this list that you need more urgently?",
"If we could only ship ONE of these this quarter, which should it be and why?",
"Would any of these features make you upgrade to a higher plan?",
],
"responses_per_persona": 2,
}).json()
for _ in range(25):
time.sleep(5)
data = requests.get(f"{MV_BASE}/focus-groups/{fg['id']}", headers=MV_H).json()
if data.get("status") == "completed":
break
print(f"\n{'='*60}\nFOCUS GROUP VALIDATION\n{'='*60}")
for resp in data.get("responses", []):
idx = persona_ids.index(resp.get("persona_id")) if resp.get("persona_id") in persona_ids else -1
name = SEGMENTS[idx]["name"] if 0 <= idx < len(SEGMENTS) else "Unknown"
print(f"\n[{name}] Q: {resp.get('question','')[:80]}")
print(f" A: {resp.get('answer','')[:350]}")
const SL_TOKEN = process.env.SLACK_BOT_TOKEN;
const SL_BASE = "https://slack.com/api";
const SL_H = { Authorization: `Bearer ${SL_TOKEN}` };
const MV = process.env.MAVERA_API_KEY;
const MV_BASE = "https://app.mavera.io/api/v1";
const MV_H = { Authorization: `Bearer ${MV}`, "Content-Type": "application/json" };
const CHANNEL_ID = "C0123FEEDBACK";
const DAYS_BACK = 60;
const oldest = String(Math.floor(Date.now() / 1000) - DAYS_BACK * 86400);
// 1. Fetch
const messages = [];
let cursor;
do {
const params = new URLSearchParams({ channel: CHANNEL_ID, limit: "200", oldest });
if (cursor) params.set("cursor", cursor);
const data = await (await fetch(`${SL_BASE}/conversations.history?${params}`, { headers: SL_H })).json();
if (!data.ok) break;
messages.push(...(data.messages || []));
cursor = data.response_metadata?.next_cursor;
await new Promise(r => setTimeout(r, 1000));
} while (cursor);
const feedback = messages.filter(m => m.type === "message" && !m.bot_id && (m.text||"").length > 15);
console.log(`Feedback messages: ${feedback.length}`);
// 2. Themes
const corpus = feedback.slice(-60).map(m => `- ${(m.text||"").slice(0,300)}`).join("\n");
const themes = await fetch(`${MV_BASE}/mave/chat`, { method: "POST", headers: MV_H,
body: JSON.stringify({ message: `Top 6 feature themes from ${feedback.length} messages.\n\n${corpus.slice(0,6000)}\n\nPer theme: name, frequency, quote, impact (1-10). Numbered list.` }),
}).then(r => r.json());
const themesText = themes.content || "";
console.log(`Themes:\n${themesText.slice(0, 1000)}`);
// 3. Personas
const SEGMENTS = [
{ name: "Power User (Daily)", desc: "4+ hours/day. Workarounds. Values efficiency." },
{ name: "Casual User (Weekly)", desc: "2-3x/week. Needs simplicity. Intuitive UX." },
{ name: "Team Admin", desc: "Manages 20+ members. Permissions, reporting." },
{ name: "New User (< 30 days)", desc: "Just started. Learning. Compares to previous tools." },
];
const personaIds = [];
for (const seg of SEGMENTS) {
const p = await fetch(`${MV_BASE}/personas`, { method: "POST", headers: MV_H,
body: JSON.stringify({ name: `Feedback: ${seg.name}`, description: seg.desc }),
}).then(r => r.json());
personaIds.push(p.id);
await new Promise(r => setTimeout(r, 300));
}
// 4. Focus Group
const fg = await fetch(`${MV_BASE}/focus-groups`, { method: "POST", headers: MV_H,
body: JSON.stringify({ name: "Product Feedback Validation", persona_ids: personaIds,
questions: [
`Top requests:\n${themesText.slice(0,1500)}\n\nBiggest difference for YOU?`,
"Feature NOT listed that you need more?",
"Ship ONE this quarter — which and why?",
"Would any make you upgrade plans?",
], responses_per_persona: 2 }),
}).then(r => r.json());
let data;
for (let i = 0; i < 25; i++) {
await new Promise(r => setTimeout(r, 5000));
data = await fetch(`${MV_BASE}/focus-groups/${fg.id}`, { headers: MV_H }).then(r => r.json());
if (data.status === "completed") break;
}
console.log(`\n${"=".repeat(60)}\nFOCUS GROUP VALIDATION`);
for (const resp of data.responses || []) {
const idx = personaIds.indexOf(resp.persona_id);
const name = idx >= 0 ? SEGMENTS[idx].name : "Unknown";
console.log(`\n[${name}] Q: ${(resp.question||"").slice(0,80)}`);
console.log(` A: ${(resp.answer||"").slice(0,350)}`);
}
Example Output
Feedback messages: 187
Themes:
1. Dark Mode (28 mentions, impact: 6/10)
2. API Webhooks (22 mentions, impact: 8/10)
3. Better Mobile App (19 mentions, impact: 7/10)
4. Custom Dashboards (15 mentions, impact: 9/10)
5. SSO for Teams (12 mentions, impact: 8/10)
6. Bulk Export (9 mentions, impact: 5/10)
FOCUS GROUP VALIDATION
============================================================
[Power User] Q: Ship ONE this quarter?
A: API Webhooks. Dark mode is cosmetic. I need webhooks to automate
my workflow — currently polling every 5 minutes which is brittle.
Webhooks unlock an entire integration ecosystem.
[New User] Q: Ship ONE this quarter?
A: Better Mobile App. I'm still learning the desktop version and
the mobile app feels like a different product. If onboarding
was mobile-first, I'd use it way more.
[Team Admin] Q: Would any make you upgrade?
A: Custom Dashboards. I currently export to Google Sheets to build
reports for leadership. Native dashboards = immediate upgrade.
Error Handling
Low feedback volume
Low feedback volume
Channels with fewer than 20 messages in 60 days may not produce meaningful themes. Extend
DAYS_BACK or combine with related channels.Emoji reactions as data
Emoji reactions as data
Slack reactions (thumbsup, fire, etc.) indicate agreement. Count reactions on feedback messages to weight importance. Access via the
reactions field in message objects.Focus Group timeout
Focus Group timeout
Complex questions with 4+ personas may take 60-90 seconds. The polling loop allows up to 125 seconds.