ingest-user-interview

SkillFiles & storage

Analyze user interview transcripts (Granola, pasted text, or a file) through a 3-analyst-agent plus Opus-synthesis pipeline. Say ''process the interview'', ''onboarding interview'', ''research call''.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the ingest-user-interview skill

What this skill tells your AI

The instructions your AI receives, as published by egregore-labs/egregore in .codex/skills/ingest-user-interview/SKILL.md and read by ahel’s review.

Analyze user interview transcripts. Uses a multi-dimensional analysis pipeline with 3 analyst agents + Opus synthesis to extract rich, structured insights from user research sessions.

Arguments: $ARGUMENTS (Optional: search term, file path, or "synthesis" for cross-interview patterns)

Usage

  • /ingest user-interview — Interactive: choose source (Granola, paste, file)
  • /ingest user-interview <path> — Process a specific transcript file
  • /ingest user-interview <search> — Find interview in Granola by title

When to invoke

Trigger phrases:

  • "process the interview", "analyze the user interview", "ingest the interview"
  • "onboarding interview", "user research session", "research call"
  • "user feedback session", "interview with [name]"

Architecture

Multi-dimensional analysis pipeline with 3 Sonnet analyst agents + Opus synthesis:

Input (transcript) → Pass 0 (Opus, inline): scaffold
Cross-interview context → 4 Neo4j queries → graph context
                ┌─────────────────────────────────────────┐
          JOURNEY (Sonnet)    SENTIMENT (Sonnet)    PRODUCT (Sonnet)
          transcript+scaffold  transcript (fresh)    transcript+scaffold+quests
          friction, aha,       emotions, confusion,  feature discovery,
          task flow, stuck     delight, frustration,  mental models,
          points, drop-off     engagement arc         unmet needs
                └──────────────┬──────────────────────┘
                    SYNTHESIS (Opus, inline)
                    → Interview Analysis Briefing
                    → Enriched insight list

Cost & Resource Budget

Target per interview:

  • Bash calls: ~6-10 (source fetch, graph batches, file writes, git)
  • Task agents: 3 Sonnet (parallel, inline — NOT background)
  • AskUserQuestion: 0-2 (source selection, participant info)
  • Graph batches: 2-3 (1 context read, 1-2 artifact write batches at <=20 queries each)
  • Token-heavy: Journey + Sentiment agents receive full transcript (~6K words each)

What to do

Canonical ingest projection (mandatory)

Write each approved interview and its extracted artifacts to one durable egregore-knowledge-projection/v1 manifest under memory/ingest/knowledge/interviews/, then validate and apply it:

bash bin/ingest-graph.sh validate "<manifest>"
bash bin/ingest-graph.sh apply "<manifest>"

The manifest is the authoritative graph write contract. Raw graph batches later in this document are schema reference for legacy data and must not be executed as a second write path. Known researchers and participants link existing members; others require explicit source-scoped external identities.

Step 0: Source selection

Determine transcript source from $ARGUMENTS:

If $ARGUMENTS is a file path (contains / or ends in .txt/.md/.json): Read the file directly. Parse for speaker turns.

If $ARGUMENTS is a search term (non-empty, not a path): Search Granola via MCP: use the get_meetings MCP tool to search by title/content. If one match, use it. If multiple, present via AskUserQuestion. If none, say so.

If $ARGUMENTS is empty, present source picker:

AskUserQuestion:
  question: "Where is the interview transcript?"
  header: "Source"
  options:
    - label: "Granola recording"
      description: "Select from recent Granola meetings"
    - label: "Paste transcript"
      description: "Paste the interview text directly"
    - label: "File path"
      description: "Provide a path to a .txt, .md, or .json transcript file"

For Granola source: Check Granola MCP availability: use ToolSearch with query "granola" to check if MCP tools are loaded.

If not available: "Granola not connected. Run /connect granola to set it up."

If available: use the list_meetings MCP tool to get recent meetings, then present via AskUserQuestion for selection. Use get_meeting_transcript to fetch the selected interview's transcript.

For paste source: Tell the user: "Paste the interview transcript below. I'll look for speaker turns (Name: or [timestamp] patterns)."

Wait for user to paste. Parse the pasted text for structure.

For file path source: Ask the user for the file path, then read it.

Step 0.5: Gather participant info

Ask for participant details if not inferrable from transcript:

AskUserQuestion:
  question: "Who was interviewed? (first name or handle)"
  header: "Participant"
  options:
    - label: "Enter name"
      description: "The person being interviewed (not the researcher)"

Also determine:

  • Researcher: default to user's name from .egregore-state.jsonname
  • Interview type: infer from context, or ask:
AskUserQuestion:
  question: "What type of interview is this?"
  header: "Type"
  options:
    - label: "Onboarding"
      description: "First-time user setup and initial experience"
    - label: "Feedback"
      description: "Existing user sharing thoughts on the product"
    - label: "Research"
      description: "Exploratory conversation about needs/workflows"
    - label: "Exit"
      description: "User leaving or churning — understanding why"

Step 1: Load cross-interview context (single batch call)

Run 4 queries in a single bash bin/graph-batch.sh call:

bash bin/graph-batch.sh '[
  {"statement": "MATCH (a:Artifact) WHERE a.origin STARTS WITH '"'"'interview:'"'"' AND a.created >= datetime() - duration('"'"'P90D'"'"') RETURN a.id, a.title, a.type, a.topics, a.interviewParticipant, a.interviewDate, a.confidence ORDER BY a.created DESC LIMIT 15"},
  {"statement": "MATCH (a:Artifact) WHERE a.origin STARTS WITH '"'"'interview:'"'"' AND a.type = '"'"'friction'"'"' WITH a.title AS friction, count(*) AS mentions, collect(DISTINCT a.interviewParticipant)[..5] AS participants WHERE mentions >= 2 RETURN friction, mentions, participants ORDER BY mentions DESC LIMIT 10"},
  {"statement": "MATCH (a:Artifact) WHERE a.origin STARTS WITH '"'"'interview:'"'"' AND a.topics IS NOT NULL UNWIND a.topics AS topic WITH topic, count(DISTINCT a.interviewParticipant) AS participantCount, collect(DISTINCT a.interviewParticipant)[..5] AS participants WHERE participantCount >= 2 RETURN topic, participantCount, participants ORDER BY participantCount DESC LIMIT 10"},
  {"statement": "MATCH (q:Quest {status: '"'"'active'"'"'}) WHERE q.topics IS NOT NULL RETURN q.id, q.title, q.topics"}
]'

Result mapping:

  • results[0] → Q1: Recent interview-extracted artifacts (90d)
  • results[1] → Q2: Recurring friction points across participants
  • results[2] → Q3: Feature/topic patterns across participants
  • results[3] → Q4: Active quests (for linking)

If the batch call fails, continue without cross-interview context. It's enrichment, not required. Empty results are expected for the first few interviews — handle gracefully.

Step 2: Pass 0 — Interview Scaffold (Opus, inline)

Read the transcript text (panel text if Granola, pasted text, or file contents). Produce a scaffold — a JSON array of extracted items:

[
  {"id": "s1", "category": "friction", "title": "Confused by setup step 3", "brief": "Participant couldn't find where to enter API key", "gap": false},
  {"id": "s2", "category": "aha", "title": "Discovered shared memory", "brief": "Moment of clarity when they saw how memory persists", "gap": false},
  {"id": "s3", "category": "feature_gap", "title": "Wanted team dashboard", "brief": "Expected to see team activity overview", "gap": false},
  {"id": "gap1", "category": "unknown", "title": "Mentioned something about permissions", "brief": "Referenced but not elaborated", "gap": true}
]

Classification signals:

CategorySignalsExample
frictionstruggled, confused, couldn't find, "I don't understand", repeated attempts, pauses, sighs"Couldn't find the API key field"
aha"oh!", "I see", excitement, sudden understanding, "that's cool", "wait, so...""Realized memory persists across sessions"
feature_gap"I expected", "I wish", "where is the...", "can I...", "it would be nice""Expected a team activity dashboard"
mental_modelassumptions about how things work, analogies used, vocabulary choices"Thought memory was per-session like ChatGPT"
feature_discoveryfound something unexpected, "oh I didn't know", positive surprise"Discovered /activity command by accident"
suggestionexplicit suggestions, "you should", "it would help if", "what if""Could you add email notifications?"
unknownReferenced but not elaborated — gaps for transcript to fill"Something about permissions"

Gap items: Things hinted at but not elaborated. Mark with "gap": true — the transcript pass should look for these specifically.

Don't extract trivia. Only insights worth preserving for product development.

Step 3: Dispatch 3 analyst agents (parallel, Sonnet)

Spawn 3 Sonnet sub-agents in parallel using the Task tool with model: "sonnet" and subagent_type: "general-purpose". Do NOT use run_in_background: true — run them as standard parallel Task calls so results are returned directly.

Agent 1: Journey Analyst

Input: transcript + scaffold from Pass 0 + Q1 results (recent interview artifacts) + participant's previous interview data (if any from Q1)

Task tool prompt (substitute actual data for placeholders):

You are the Journey Analyst for a user interview analysis pipeline. Your job is to map the participant's JOURNEY through the product — where they got stuck, where they had breakthroughs, what tasks they tried to complete, and where they dropped off or got lost.

## Participant

{INSERT PARTICIPANT NAME + INTERVIEW TYPE}

## Scaffold (from initial pass)

{INSERT SCAFFOLD JSON}

## Previous Interview Artifacts (from this and other participants)

{INSERT Q1 RESULTS — or "No previous interview data. This is an early interview." if empty}

## Transcript

{INSERT TRANSCRIPT}

## Instructions

Analyze the transcript through the lens of JOURNEY: what the participant tried to do, where they succeeded or failed, what path they took through the product.

Return a JSON object with this structure:

{
  "journey_stages": [
    {"stage": "...", "description": "free-form: what the participant was trying to do and what happened",
     "task": "...", "outcome": "completed|abandoned|struggled|skipped",
     "friction_points": [{"description": "...", "severity": "blocker|frustration|minor", "quote": "..."}],
     "aha_moments": [{"description": "...", "quote": "..."}],
     "time_spent_estimate": "brief|moderate|extended",
     "assistance_needed": true}
  ],
  "critical_path": [
    {"step": "...", "description": "free-form: the ideal path vs what actually happened",
     "deviation": "on_track|minor_detour|major_detour|lost"}
  ],
  "drop_off_risks": [
    {"point": "...", "description": "free-form: where and why the user might give up",
     "severity": "high|medium|low", "quote": "..."}
  ],
  "task_completion": {
    "attempted": 0,
    "completed": 0,
    "abandoned": 0,
    "success_rate_description": "free-form: overall assessment of task completion"
  },
  "enrichments": [
    {"scaffold_id": "s1|new", "category": "friction|aha|feature_gap|mental_model|feature_discovery|suggestion",
     "title": "...", "brief": "...", "evidence_quote": "...",
     "speaker": "participant|researcher",
     "severity": "blocker|frustration|minor|null",
     "stage": "which journey stage this belongs to"}
  ],
  "_raw_notes": "Multi-paragraph prose: your full read on the participant's journey. Include the narrative arc — where they started, where they ended up, what the experience felt like from their perspective. Note patterns that connect to previous interview data if available."
}

## Rules

1. Match scaffold items to transcript segments. For gap items (gap: true), try especially hard to find evidence.
2. Extract the richest quote — prioritize signal over length. Max 120 chars per quote.
3. Find items the scaffold missed — things discussed substantively but not in the scaffold. Use scaffold_id: "new".
4. Do NOT extract small talk, logistics, or off-topic tangents.
5. If a scaffold item has no transcript evidence, omit it from enrichments (don't fabricate).
6. Tag fields are optional suggestions. If no predefined tag fits, leave null and let description carry signal.
7. The _raw_notes section is critical — this is where your nuanced journey narrative goes.

Return ONLY valid JSON. No markdown fences, no explanation.
Agent 2: Sentiment Analyst

Input: transcript + participant context. NO scaffold, NO graph (fresh emotional read).

Task tool prompt:

You are the Sentiment Analyst for a user interview analysis pipeline. Your job is to read the EMOTIONAL texture of the interview — what the participant felt, where they were confused or delighted or frustrated. You receive NO prior analysis intentionally — we want a fresh emotional read without anchoring.

## Participant

{INSERT PARTICIPANT NAME + INTERVIEW TYPE}

## Transcript

{INSERT TRANSCRIPT}

## Instructions

Analyze the transcript through the lens of SENTIMENT: what the participant felt, how their emotions evolved, where they experienced confusion, delight, or frustration.

Return a JSON object with this structure:

{
  "emotional_arc": {
    "opening": "free-form: emotional state at the start of the interview",
    "closing": "free-form: emotional state at the end",
    "trajectory": "free-form: how emotions evolved throughout",
    "peaks": [
      {"moment": "...", "emotion": "...", "intensity": "high|medium|low", "quote": "..."}
    ]
  },
  "confusion_signals": [
    {"description": "free-form: what confused the participant and how they expressed it",
     "quote": "...", "resolution": "resolved|unresolved|workaround"}
  ],
  "delight_signals": [
    {"description": "free-form: what delighted or positively surprised the participant",
     "quote": "..."}
  ],
  "frustration_signals": [
    {"description": "free-form: what frustrated the participant and how they expressed it",
     "quote": "...", "severity": "high|medium|low"}
  ],
  "engagement_assessment": {
    "description": "free-form: how engaged the participant was overall",
    "level": "high|medium|low|mixed",
    "energy_moments": [
      {"moment": "...", "energy": "high|low", "quote": "..."}
    ]
  },
  "trust_signals": [
    {"description": "free-form: moments indicating trust or distrust in the product/team",
     "direction": "trust|distrust", "quote": "..."}
  ],
  "_raw_notes": "Multi-paragraph prose: your full emotional read of the interview. What was the participant's overall experience? Where did they feel supported vs abandoned? What was unsaid? Where did energy spike or drop? What did their tone reveal that their words didn't?"
}

## Rules

1. Read emotions from word choice, pacing, emphasis, hesitation, laughter, sighs — not just explicit statements.
2. Distinguish between expressed emotions and inferred emotions. Be explicit about which is which.
3. Look for emotional transitions — moments where the participant's state shifted.
4. The _raw_notes section is where your real analysis lives. Be honest about uncertainty.
5. Pay attention to what the participant does NOT say — avoidance can signal discomfort.
6. Note the researcher's emotional impact — did their questions help or hinder openness?

Return ONLY valid JSON. No markdown fences, no explanation.
Agent 3: Product Insights Analyst

Input: transcript + scaffold from Pass 0 + Q3 results (feature/topic patterns) + Q4 results (active quests)

Task tool prompt:

You are the Product Insights Analyst for a user interview analysis pipeline. Your job is to extract PRODUCT insights — what features the participant discovered or wanted, what mental models they brought, what unmet needs they expressed, and how their experience maps to active product development.

## Participant

{INSERT PARTICIPANT NAME + INTERVIEW TYPE}

## Scaffold (from initial pass)

{INSERT SCAFFOLD JSON}

## Feature/Topic Patterns Across Participants

{INSERT Q3 RESULTS — or "No cross-participant patterns yet. This is an early interview." if empty}

## Active Quests

{INSERT Q4 RESULTS — or "No active quests." if empty}

## Transcript

{INSERT TRANSCRIPT}

## Instructions

Analyze the transcript through the lens of PRODUCT: what the participant's experience reveals about features, mental models, unmet needs, and product direction.

Return a JSON object with this structure:

{
  "feature_discovery": [
    {"feature": "...", "description": "free-form: how the participant discovered/used this feature",
     "reaction": "positive|negative|neutral|confused",
     "quote": "...", "discoverability": "intuitive|guided|accidental|missed"}
  ],
  "mental_model_mismatches": [
    {"expected": "what the participant thought would happen or exist",
     "actual": "what actually happens or exists",
     "description": "free-form: the mismatch and its impact on the experience",
     "quote": "...", "severity": "high|medium|low"}
  ],
  "unmet_needs": [
    {"need": "...", "description": "free-form: what the participant needs that the product doesn't provide",
     "evidence_type": "explicit_request|implicit_behavior|workaround|comparison",
     "quote": "...", "priority_signal": "high|medium|low"}
  ],
  "suggestions": [
    {"suggestion": "...", "description": "free-form: what the participant suggested and the underlying need",
     "quote": "...", "feasibility_note": "free-form: quick assessment of implementation complexity"}
  ],
  "enrichments": [
    {"scaffold_id": "s1|new", "category": "friction|aha|feature_gap|mental_model|feature_discovery|suggestion",
     "title": "...", "brief": "...", "evidence_quote": "...",
     "speaker": "participant|researcher",
     "quest_link": "quest-id or null",
     "priority_tag": "p0_blocker|p1_important|p2_nice_to_have|null"}
  ],
  "competitive_signals": [
    {"competitor": "...", "description": "free-form: what the participant compared to or referenced",
     "quote": "...", "implication": "free-form: what this means for our product"}
  ],
  "_raw_notes": "Multi-paragraph prose: your full product read of the interview. What does this participant's experience tell us about where the product is strong, where it's weak, and where it should go next? Connect to active quests if relevant. Note cross-participant patterns if provided."
}

## Rules

1. Match scaffold items to transcript segments. For gap items (gap: true), try especially hard to find evidence.
2. Extract the richest quote — prioritize signal over length. Max 120 chars per quote.
3. Find items the scaffold missed — things discussed substantively but not in the scaffold. Use scaffold_id: "new".
4. Do NOT extract trivia or off-topic tangents.
5. If a scaffold item has no transcript evidence, omit it from enrichments (don't fabricate).
6. Tag fields are optional suggestions. If no predefined tag fits, leave null and let description carry signal.
7. For quest_link: match artifact topics to active quest topics. Only link if genuinely relevant.
8. priority_tag: p0 = blocks adoption, p1 = significantly impacts experience, p2 = would be nice.
9. The _raw_notes section is critical — this is where your nuanced product analysis goes.

Return ONLY valid JSON. No markdown fences, no explanation.

Parse all 3 agent results: Extract the JSON from each response. If an agent returns invalid JSON or fails, log the failure and continue with whatever agents succeeded. The synthesis step works with partial input.

Step 4: Synthesis (Opus, inline)

Read the 3 agent outputs + scaffold. Produce two things:

1. Enriched insight list

For each scaffold item (and new items from enrichments), merge dimensional data from all three agents:

  • From Journey: friction points, aha moments, task completion, drop-off risks, stage context
  • From Sentiment: emotional peaks, confusion/delight/frustration signals, engagement level
  • From Product: feature discovery, mental model mismatches, unmet needs, quest links, priority

Each merged insight gets:

  • category: from scaffold (or enrichment for new items)
  • title: from scaffold (or enrichment)
  • content: synthesized from scaffold brief + Journey context + Product context
  • severity: from Journey (blocker/frustration/minor) or Product (p0/p1/p2)
  • emotional_context: from Sentiment — what the participant felt at this moment
  • evidence_quote: best quote from any agent
  • speaker: "participant" or "researcher"
  • topics: 2-5 tags derived from content
  • stage: from Journey — where in the journey this occurred
  • quest_link: from Product — linked quest if relevant
  • priority_tag: from Product (p0_blocker/p1_important/p2_nice_to_have)
  • cross_participant: boolean — does this echo a pattern from Q2/Q3?
  • confidence: based on evidence strength (0.9 = explicit + emotional alignment, 0.7 = explicit only, 0.5 = inferred)

Separate action items (things the researcher committed to) from insights. Actions don't become Artifact files.

2. Interview Analysis Briefing

Synthesize all agent outputs into a coherent briefing document:

Meta-Analysis synthesis guidance:

Write 3-5 paragraphs of opinionated analysis. Not a summary — a reading of the interview that tells someone something they wouldn't get from skimming the transcript.

Structure around four lenses:

  1. Heart of the interview. What was this really about? Not the interview guide — the actual gravitational center. What is this participant struggling with, excited about, or trying to tell us? First paragraph, get there fast.

  2. Journey assessment. How did the participant's experience map to what we designed? Where did reality diverge from intent? What was the critical path and where did it break? Reference specific friction points and aha moments.

  3. Emotional read. What did the participant FEEL? Not just what they said — what their tone, pace, energy, and word choice revealed. Where did we earn trust and where did we lose it? What went unsaid?

  4. Product implications. What should change based on this interview? Be specific — not "improve onboarding" but "step 3 needs a visual indicator for the API key field because 2/3 participants missed it." Connect to active quests. Distinguish blockers from nice-to-haves.

Register: A researcher briefing the team. Direct, evidence-based, doesn't over-generalize from one participant but names what's worth paying attention to.

Anti-patterns:

  • "The interview covered several areas..." → you have nothing to say
  • Restating what's in the insight list → the list exists for that
  • Equal weight to everything discussed → prioritize ruthlessly
  • "We should consider improving..." → who, what, by when, or don't say it
  • Over-generalizing from one participant → "this participant experienced X" not "users experience X"
# Interview Analysis: {Participant} — {Type} Interview

**Date**: YYYY-MM-DD
**Participant**: {name}
**Researcher**: {researcher}
**Type**: {onboarding|feedback|research|exit}
**Source**: {Granola (doc-id) | file path | pasted}
**Emotional Arc**: {opening} → {closing}
**Engagement**: {level}

## Meta-Analysis

{3-5 paragraphs — heart of the interview, journey assessment, emotional read, product implications. See guidance above.}

## Journey Map

{From Journey analyst — stage by stage}

| Stage | Task | Outcome | Friction | Aha | Time |
|-------|------|---------|----------|-----|------|
| ... | ... | completed/abandoned | count | count | brief/extended |

### Critical Path

{From Journey analyst}
- {step}: {on_track|deviation} — {description}

### Drop-Off Risks

{From Journey analyst}
- **{point}** ({severity}): {description}
  > "{quote}"

## Friction Points

{Merged from all analysts}

Shortened here. Read the whole file on GitHub.

Signals

GitHub stars
288
Forks
21
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
ingest-user-interview
Source
github.com/egregore-labs/egregore