cosmicstack-labs/user research
A comprehensive skill for user research — covering research methods, interview techniques, participant recruitment, synthesis, insight generation, readout formats, and continuous discovery habits. From early generative research to evaluative usability testing.
npx skills add https://github.com/cosmicstack-labs/mercury-agent-skills --skill User Research
Great user research produces not just data, but decision-grade insights. These principles underpin every effective research practice:
Understand your organization's research maturity to identify the right next step.
| Level | Name | Characteristics |
|-------|------|-----------------|
| 1 | Ad-hoc | No dedicated researcher. Decisions based on intuition or stakeholder opinions. Research is reactive and rare. |
| 2 | Foundational | Occasional usability tests and surveys. Research is done but not systematic. Findings may not influence decisions. |
| 3 | Operational | Dedicated research function exists. Research is planned quarterly. Methods are consistent. Findings reach product teams. |
| 4 | Integrated | Research is embedded in every product team. Continuous discovery is practiced. Researchers participate in roadmap decisions. |
| 5 | Strategic | Research drives company strategy. Research-led innovation is standard. The org runs experiments to test strategic hypotheses. |
Quick self-assessment: When was the last time a product decision was reversed or changed because of user research? If you can't recall a specific example, you're likely at Level 1 or 2.
| Dimension | Generative | Evaluative |
|-----------|------------|------------|
| Goal | Discover what problems to solve | Validate whether a solution works |
| When | Early, before solutions exist | During or after design/development |
| Questions | "What do users struggle with?" | "Can users complete this task?" |
| Methods | Interviews, diary studies, contextual inquiry | Usability testing, A/B testing, desirability studies |
| Output | Opportunity areas, user needs, personas | Usability issues, satisfaction scores, task success rates |
| Dimension | Qualitative | Quantitative |
|-----------|-------------|--------------|
| Sample size | Small (5-30) | Large (100+) |
| Output | Themes, stories, behaviors, motivations | Numbers, statistics, benchmarks |
| Strengths | Deep understanding of "why" | Reliable measurements of "how many" and "how often" |
| Weaknesses | Can't generalize broadly | Lacks context and depth |
| Common methods | Interviews, observations, diary studies | Surveys, analytics, A/B tests |
Rule of thumb: Use qualitative to discover *what* matters and *why*. Use quantitative to measure *how much* it matters and to *whom*.
| Question you're trying to answer | Recommended method |
|----------------------------------|-------------------|
| What problems do users face? | Generative interviews, diary study |
| How do users currently accomplish X? | Contextual inquiry, diary study |
| Can users complete this flow? | Usability testing (moderated) |
| Which design do users prefer? | A/B test, desirability study |
| How satisfied are users overall? | Survey (e.g., SUS, NPS, CSAT) |
| What language do users use? | Search log analysis, interview transcripts |
| Is the problem big enough to solve? | Survey, analytics, competitive analysis |
A fully scripted interview where every question is asked in the same order to every participant.
When to use: Large-scale studies where comparability across participants is critical. Late-stage evaluation.
Pros: Highly comparable data, lower moderator bias, easier for novice moderators.
Cons: Rigid — can't follow interesting threads. Misses serendipitous discoveries.
A guided conversation with a prepared question framework but the freedom to probe and follow tangents.
When to use: Most generative and discovery research. This is the gold standard for most product research.
Structure:
Probing techniques:
# Semi-structured interview guide template
def generate_interview_guide(research_questions: list, method: str = "semi_structured") -> dict:
"""
Generate an interview guide from research questions.
Maps each research question to specific interview questions.
"""
guide = {
"metadata": {
"method": method,
"estimated_duration_minutes": 45,
"created_for": "discovery_round_1"
},
"sections": [
{
"name": "Warm-up",
"duration_minutes": 5,
"purpose": "Build rapport and establish context",
"questions": [
"Tell me a little about your role and what you do day-to-day.",
"What tools do you use most frequently in your work?"
]
},
{
"name": "Context",
"duration_minutes": 10,
"purpose": "Understand current behavior",
"questions": [
"Walk me through the last time you needed to [core activity].",
"What was working well? What was frustrating?"
]
},
{
"name": "Deep Dive",
"duration_minutes": 20,
"purpose": "Explore specific behaviors and motivations",
"questions": [
"Tell me about a time when [specific scenario] happened.",
"What did you do next?",
"Why was that important to you?"
]
},
{
"name": "Reflection",
"duration_minutes": 10,
"purpose": "Uncover unmet needs and aspirations",
"questions": [
"If you could wave a magic wand, what would change about this process?",
"What have you tried that didn't work?"
]
}
],
"probes": [
"Can you tell me more about that?",
"What happened next?",
"How did that make you feel?",
"Walk me through that step by step."
]
}
return guide
A field research method where you observe users in their natural environment while they work. You ask questions *while* they work.
Core principles (the four C's):
When to use: Understanding complex workflows, physical environments, collaborative tasks, or processes that are hard to articulate from memory.
Example setup:
def contextual_inquiry_plan(research_goal: str, participant_role: str) -> dict:
"""
Plan a contextual inquiry session.
"""
return {
"research_goal": research_goal,
"participant_role": participant_role,
"session_structure": {
"introduction": {
"duration": 10,
"activities": [
"Explain that you're there to learn from them",
"Ask them to work as they normally would",
"Let them know you may interrupt to ask questions"
]
},
"observation": {
"duration": 30,
"activities": [
"Observe silently for 5 minutes initially",
"Ask 'what are you doing right now?' frequently",
"Ask 'why did you do that?' when interesting moments occur",
"Take photos (with permission) of the workspace",
"Note tools, artifacts, workarounds"
]
},
"debrief": {
"duration": 10,
"activities": [
"Summarize what you observed",
"Ask 'is there anything I missed?'",
"Thank the participant"
]
}
},
"equipment_needed": [
"Notebook and pen (backup)",
"Recording device (with consent)",
"Camera for workspace photos",
"List of research questions printed"
]
}
A screener survey filters participants to ensure you talk to the right people.
Screener best practices:
# Screener survey data model
class Screener:
def __init__(self, study_name: str, ideal_participant_profile: dict):
self.study_name = study_name
self.profile = ideal_participant_profile # e.g., {"role": "PM", "company_size": "50-500"}
self.questions = []
self.passing_criteria = []
def add_question(self, question_text: str, question_type: str,
options: list = None, passing_answer: str = None):
self.questions.append({
"text": question_text,
"type": question_type, # multiple_choice, scale, open_text
"options": options,
"passing_answer": passing_answer
})
def is_qualified(self, responses: dict) -> tuple:
"""
Returns (qualified: bool, reason: str).
"""
for q in self.questions:
if q["passing_answer"] and responses.get(q["text"]) != q["passing_answer"]:
return False, f"Failed: {q['text']}"
return True, "Qualified"
def calculate_qualification_rate(self, all_responses: list) -> float:
qualified = sum(1 for r in all_responses if self.is_qualified(r)[0])
return qualified / len(all_responses) if all_responses else 0
Incentives must be appropriate for the audience, the time commitment, and the difficulty of recruitment.
| Participant type | 30-min interview | 60-min interview | Diary study (1 week) |
|-----------------|-----------------|------------------|---------------------|
| General consumers | $25-$50 | $50-$100 | $100-$200 |
| Professionals (e.g., PMs, engineers) | $50-$100 | $100-$150 | $200-$300 |
| Executives / Specialists | $100-$200 | $200-$400 | $500+ |
| B2B (enterprise) | $75-$150 | $150-$250 | $300-$500 |
Pro tip: Send gift cards within 24 hours of the session. Late incentives damage your recruitment pipeline.
The right sample size depends on your method and goals.
| Method | Recommended n | Why |
|--------|--------------|-----|
| Generative interviews | 8-15 per segment | Saturation typically occurs around 8-12 interviews |
| Usability testing | 5-8 per test | Nielsen's law: 5 users uncover ~85% of usability issues |
| Survey | 100-400+ per segment | Depends on desired confidence interval and population size |
| Diary study | 8-15 per segment | Attrition is common; overshoot by 20-30% |
| Card sorting | 20-30 per segment | 20+ participants stabilizes the similarity matrix |
The saturation rule: Stop recruiting when you stop hearing new things. If interviews 3-5 in a row surface no new themes, you've likely reached saturation.
Synthesis transforms raw data into structured insights. It is the hardest and most valuable part of research.
A bottom-up synthesis technique where individual observations (notes, quotes, behaviors) are grouped into themes.
Process:
Tools: Miro, FigJam, MURAL for digital. Sticky notes and walls for physical.
# Affinity map data structure
class AffinityMap:
def __init__(self, study_name: str):
self.study_name = study_name
self.raw_notes = [] # [(participant_id, note_text, category)]
self.themes = {} # {theme_name: [note_indices]}
self.insights = [] # Synthesized insights
def add_note(self, participant_id: str, note_text: str, category: str = ""):
self.raw_notes.append((participant_id, note_text, category))
def cluster_notes(self, clusters: dict):
"""clusters = {theme_name: [note_index, ...]}"""
self.themes = clusters
def generate_insight(self, theme_name: str, insight_text: str, confidence: float):
self.insights.append({
"theme": theme_name,
"insight": insight_text,
"supporting_notes": self.themes.get(theme_name, []),
"confidence": confidence
})
def get_insights_by_confidence(self, min_confidence: float = 0.7):
return [i for i in self.insights if i["confidence"] >= min_confidence]
A rigorous, structured approach to identifying patterns in qualitative data. Adapted from Braun & Clarke's 6-phase framework.
Phases:
A journey map visualizes a user's experience over time — capturing actions, thoughts, emotions, and pain points.
Components of a good journey map:
# Journey map data model
class JourneyMap:
def __init__(self, persona: str, scenario: str):
self.persona = persona
self.scenario = scenario
self.phases = []
def add_phase(self, name: str):
self.phases.append(JourneyPhase(name))
def get_opportunities(self):
"""Aggregate opportunities across all phases."""
opportunities = []
for phase in self.phases:
for step in phase.steps:
if step.opportunity:
opportunities.append({
"phase": phase.name,
"step": step.action,
"opportunity": step.opportunity,
"pain_intensity": step.pain_intensity
})
return sorted(opportunities, key=lambda x: x["pain_intensity"], reverse=True)
class JourneyPhase:
def __init__(self, name: str):
self.name = name
self.steps = []
def add_step(self, action: str, thought: str, emotion: int,
pain: str = "", opportunity: str = ""):
"""
emotion: 1 (very negative) to 5 (very positive)
pain_intensity: 1 (minor) to 5 (blocking)
"""
self.steps.append({
"action": action,
"thought": thought,
"emotion": emotion,
"pain": pain,
"pain_intensity": len(pain) if pain else 0, # rough heuristic
"opportunity": opportunity
})
HMW statements transform pain points and observations into design opportunities.
Formula:
> How might we [action] for [user] so that [desired outcome]?
From observation to HMW:
| Observation | How Might We |
|-------------|-------------|
| "I have no idea if my report was received" | "HMW give users confidence that their submission was received?" |
| "I have to check five different tools to get my work done" | "HMW reduce the number of tools a user needs to complete a single task?" |
| "I always forget to back up my work" | "HMW make data backup automatic and invisible?" |
HMW brainstorming tips:
# HMW statement generator from research notes
class HMWGenerator:
def __init__(self):
self.statements = []
def from_observation(self, observation: str, user: str) -> str:
"""Convert an observation into a HMW statement."""
return f"How might we address '{observation}' for {user}?"
def from_pain_point(self, pain: str, desired_outcome: str) -> str:
"""Convert a pain point into a HMW statement."""
return f"How might we {desired_outcome} so that {pain} is eliminated?"
def generate_batch(self, observations: list, user: str) -> list:
self.statements = [self.from_observation(o, user) for o in observations]
return self.statements
def cluster_hmws(self, clusters: dict):
"""
clusters = {"theme_name": [index_of_hmw, ...]}
"""
return {
theme: [self.statements[i] for i in indices]
for theme, indices in clusters.items()
}
Opportunity areas are broader than HMWs — they describe a space where value can be created for users and the business.
Format:
> [Area name]: [Description of the opportunity]
>
> Evidence: [What research data supports this]
>
> Potential impact: [What could change for users and business]
>
> Rough sizing: [How many users affected, how frequently]
Example:
> Opportunity: Proactive Status Communication
>
> Evidence: 8/12 interview participants mentioned anxiety about not knowing whether their submission was received. Support tickets related to "did you get my X?" account for 15% of volume.
>
> Potential impact: Reduce support tickets by 15%. Increase user trust and decrease anxiety.
>
> Sizing: Affects 100% of users in the submission flow. Estimated 40,000 occurrences per month.
Different audiences need different formats. One study should produce multiple readout artifacts.
Best for: Busy executives, stakeholders who need the bottom line.
Template:
Title: [Descriptive name of the study]
Date: [Date]
Researcher(s): [Names]
Bottom line (3 bullet points max):
- Bullet 1
- Bullet 2
- Bullet 3
Key findings (5-7 with supporting evidence):
1. Finding (with 1-2 representative quotes)
2. Finding (with 1-2 representative quotes)
Recommendations (linked to findings):
→ [Recommendation 1] (addresses Finding 1 & 2)
→ [Recommendation 2] (addresses Finding 3)
Methodology: [n=X, method, dates]
Best for: Team readouts, design reviews, kickoffs.
Slide structure:
Best for: Building empathy across the org, especially for stakeholders who won't read.
Best for: Teams that need ongoing reference to research findings.
Popularized by Teresa Torres, continuous discovery is the practice of running small, frequent research activities alongside development — not as separate phases.
| Activity | Frequency | Duration | Who participates |
|----------|-----------|----------|-----------------|
| User interview | Weekly | 30 min | PM, Designer, optional Engineer |
| Opportunity review | Weekly | 30 min | Product trio (PM, Designer, Tech Lead) |
| Experiment review | Bi-weekly | 30 min | Full product team |
| Backlog refinement | Weekly | 30 min | Product trio |
The PM, Designer, and Tech Lead form the core discovery team. All three participate in interviews and synthesis together.
Why it works:
# Continuous discovery habit tracker
class DiscoveryHabits:
def __init__(self, team_name: str):
self.team_name = team_name
self.weekly_interviews = 0
self.weeks_active = 0
self.studies_completed = []
def log_interview(self, participant_role: str, method: str, insights_count: int):
self.weekly_interviews += 1
self.studies_completed.append({
"week": self.weeks_active + 1,
"participant": participant_role,
"method": method,
"insights": insights_count
})
def weekly_summary(self) -> dict:
return {
"team": self.team_name,
"interviews_this_week": self.weekly_interviews,
"total_interviews": len(self.studies_completed),
"unique_participants": len(set(s["participant"] for s in self.studies_completed)),
"avg_insights_per_session": (
sum(s["insights"] for s in self.studies_completed) /
max(len(self.studies_completed), 1)
)
}
def reset_week(self):
self.weekly_interviews = 0
self.weeks_active += 1
10. Paralysis by analysis. Spending weeks synthesizing when the key insight was clear after session 5. Good research is timely. Ship insights fast, iterate on them.
11. Not connecting research to business outcomes. "Users want X" is weak. "Improving X correlates with 20% higher retention" is powerful. Tie insights to metrics.
12. Incentive neglect. Inadequate or late incentives damage goodwill and make future recruitment harder. Treat participants fairly and generously.
*This skill is maintained by Cosmic Stack Labs. For questions or contributions, refer to the contributing guide in the repository root.*
Take cosmicstack-labs/user research from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.