Scenario
Customer-reported bugs contain language revealing pain severity beyond a priority label. This job pulls issues labeled “bug” and “customer-reported”, creates user personas for different customer segments, then runs a Mavera Focus Group asking “How critical is this bug to your workflow?” — producing a severity map driven by synthetic customer voices. Flow: Linear GraphQL (issues with labels “bug” + “customer-reported”) → MaveraPOST /personas → POST /focus-groups → Poll for results
Code
import os, requests, time
LN, MV = os.environ["LINEAR_API_KEY"], os.environ["MAVERA_API_KEY"]
LB, MB = "https://api.linear.app/graphql", "https://app.mavera.io/api/v1"
LH = {"Authorization": LN, "Content-Type": "application/json"}
MH = {"Authorization": f"Bearer {MV}", "Content-Type": "application/json"}
query = """query {
issues(filter: {
labels: { every: { name: { in: ["bug", "customer-reported"] } } }
state: { type: { nin: ["completed", "canceled"] } }
}, first: 40, orderBy: createdAt) {
nodes { identifier title description priority labels { nodes { name } } team { name } }
}
}"""
r = requests.post(LB, headers=LH, json={"query": query})
if r.status_code == 429:
time.sleep(int(r.headers.get("Retry-After", 60)))
r = requests.post(LB, headers=LH, json={"query": query})
r.raise_for_status()
bugs = r.json()["data"]["issues"]["nodes"]
print(f"Fetched {len(bugs)} customer-reported bugs")
bug_block = "\n\n".join(
f"[{b['identifier']}] {b['title']} (priority: {b.get('priority','none')})\n"
f" Team: {(b.get('team') or {}).get('name','Unknown')}\n"
f" Description: {(b.get('description') or '')[:400]}"
for b in bugs)
SEGMENTS = [
{"name": "Enterprise Admin", "desc": "Manages 200+ person org. Bugs block teams. Values stability and SLAs."},
{"name": "Startup Founder", "desc": "5-person team. Tolerates quirks, not data loss or broken integrations."},
{"name": "Product Manager", "desc": "Relies on tool for roadmap planning. Reporting bugs erode stakeholder trust."},
{"name": "Developer (IC)", "desc": "Lives in tool 8+ hrs/day. API and Git integration matter most."},
]
persona_ids = []
for seg in SEGMENTS:
time.sleep(0.3)
p = requests.post(f"{MB}/personas", headers=MH, json={
"name": f"Bug Triage: {seg['name']}", "description": seg["desc"]}).json()
persona_ids.append(p["id"])
fg = requests.post(f"{MB}/focus-groups", headers=MH, json={
"name": "Customer Bug Severity Assessment", "persona_ids": persona_ids,
"questions": [
f"Here are {len(bugs)} open bugs:\n\n{bug_block[:3000]}\n\nRate top 5 most critical for YOUR workflow (1-10). Explain.",
"Which bugs would make you consider switching to a competitor if unfixed for 30 days?",
"Any bugs that seem minor but compound into major frustration over time?",
], "responses_per_persona": 2,
}).json()
for _ in range(30):
time.sleep(5)
data = requests.get(f"{MB}/focus-groups/{fg['id']}", headers=MH).json()
if data.get("status") == "completed":
break
for resp in data.get("responses", []):
idx = persona_ids.index(resp["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]}\n A: {resp.get('answer','')[:400]}")
const LN = process.env.LINEAR_API_KEY, MV = process.env.MAVERA_API_KEY;
const LB = "https://api.linear.app/graphql", MB = "https://app.mavera.io/api/v1";
const LH = { Authorization: LN, "Content-Type": "application/json" };
const MH = { Authorization: `Bearer ${MV}`, "Content-Type": "application/json" };
const query = `query {
issues(filter: {
labels: { every: { name: { in: ["bug", "customer-reported"] } } }
state: { type: { nin: ["completed", "canceled"] } }
}, first: 40, orderBy: createdAt) {
nodes { identifier title description priority labels { nodes { name } } team { name } }
}
}`;
let res = await fetch(LB, { method: "POST", headers: LH, body: JSON.stringify({ query }) });
if (res.status === 429) {
await new Promise(r => setTimeout(r, parseInt(res.headers.get("Retry-After") || "60") * 1000));
res = await fetch(LB, { method: "POST", headers: LH, body: JSON.stringify({ query }) });
}
const bugs = (await res.json()).data?.issues?.nodes || [];
console.log(`Fetched ${bugs.length} customer-reported bugs`);
const bugBlock = bugs.map(b =>
`[${b.identifier}] ${b.title} (priority: ${b.priority || "none"})\n` +
` Team: ${b.team?.name || "Unknown"}\n Desc: ${(b.description || "").slice(0, 400)}`
).join("\n\n");
const SEGMENTS = [
{ name: "Enterprise Admin", desc: "200+ person org. Bugs block teams. Values stability." },
{ name: "Startup Founder", desc: "5-person team. Tolerates quirks, not data loss." },
{ name: "Product Manager", desc: "Roadmap planning. Reporting bugs erode trust." },
{ name: "Developer (IC)", desc: "8+ hrs/day. API and Git integration matter most." },
];
const personaIds = [];
for (const seg of SEGMENTS) {
await new Promise(r => setTimeout(r, 300));
const p = await fetch(`${MB}/personas`, { method: "POST", headers: MH,
body: JSON.stringify({ name: `Bug Triage: ${seg.name}`, description: seg.desc }),
}).then(r => r.json());
personaIds.push(p.id);
}
const fg = await fetch(`${MB}/focus-groups`, { method: "POST", headers: MH,
body: JSON.stringify({
name: "Customer Bug Severity Assessment", persona_ids: personaIds,
questions: [
`${bugs.length} open bugs:\n\n${bugBlock.slice(0,3000)}\n\nRate top 5 (1-10). Explain.`,
"Which bugs would make you switch if unfixed 30 days?",
"Any bugs that seem minor but compound into major frustration?",
], responses_per_persona: 2,
}),
}).then(r => r.json());
let data;
for (let i = 0; i < 30; i++) {
await new Promise(r => setTimeout(r, 5000));
data = await fetch(`${MB}/focus-groups/${fg.id}`, { headers: MH }).then(r => r.json());
if (data.status === "completed") break;
}
for (const resp of data.responses || []) {
const idx = personaIds.indexOf(resp.persona_id);
console.log(`\n[${idx >= 0 ? SEGMENTS[idx].name : "Unknown"}] Q: ${(resp.question||"").slice(0,80)}`);
console.log(` A: ${(resp.answer||"").slice(0, 400)}`);
}
Example Output
Fetched 17 customer-reported bugs
[Enterprise Admin] Q: Rate top 5 most critical for YOUR workflow (1-10)
A: 1. ENG-289 "SSO login fails silently after token refresh" — 10/10.
Locks out entire teams. 40 people unable to access for 2 hours.
2. ENG-301 "Webhook deliveries drop during deploys" — 9/10.
Our automation pipeline depends on webhooks.
[Startup Founder] Q: Which bugs would make you switch if unfixed 30 days?
A: ENG-289 (SSO) is a dealbreaker — we just rolled out SSO and I
can't tell my team "use password login as a workaround." Also
ENG-308 (GitHub PR links not syncing) — the Git integration is
the whole reason we chose Linear.
[Developer (IC)] Q: Any bugs that seem minor but compound?
A: ENG-322 "Keyboard shortcut 'X' occasionally triggers on wrong
issue" — I triage 50+ issues daily. One wrong archive per day
× 5 days = 5 lost issues/week.
Error Handling
Label filter matching
Label filter matching
The
every filter requires ALL listed labels. If your workspace uses “Bug” vs “bug” or “customer_reported”, adjust values. Query issueLabels to discover exact names.Focus group polling timeout
Focus group polling timeout
Complex groups with 4 personas may take 60-90s. The loop allows 150s. Check the Mavera dashboard if
status never reaches “completed”.Priority field values
Priority field values
Linear priorities are integers: 0 (none), 1 (urgent), 2 (high), 3 (medium), 4 (low). Map them to text in your prompt if personas need clearer severity context.