skills/planning-voice-agent-user-interviews/SKILL.md
Plan a round of user interviews conducted by PostHog's AI voice agent (a "robo interviewer") — the automated voice-agent interview product. Captures a UserInterviewTopic (who to target, what to ask, framing context, question list) and calls user-interview-topics-create. ONLY trigger when the user clearly wants an AI voice agent to actually run the interview calls (e.g. "set up robo user interviews", "have the voice agent interview these users"). Do NOT trigger for ordinary user research that does not involve the voice agent — finding or shortlisting users to talk to ("who'd be a good fit to interview about Y"), planning questions for a human-run interview, or analysing feedback are audience discovery, handled with normal data queries, not this skill. Also do NOT trigger for uploading a recorded interview audio file or browsing topics with user-interview-topics-list. When intent is ambiguous, first confirm what kind of research it is and whether they want an AI voice agent to conduct it (see Step 0).
npx skillsauth add posthog/ai-plugin planning-voice-agent-user-interviewsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Use this skill only when someone wants PostHog's AI voice agent — a "robo interviewer" — to actually conduct a round of user interview calls for them. The plan is captured as a UserInterviewTopic that the voice agent later runs through, calling each targeted person and working through the questions.
This is a specific product, not a generic research helper. If the user only wants to find or shortlist people to interview, plan questions for an interview a human will run, or analyse feedback they already have, this is not the right skill — handle that as ordinary audience discovery / data work (see querying-posthog-data) and, at most, mention that the voice-agent option exists.
Before doing anything else, make sure the user actually wants the AI voice agent to run the interviews. Many requests that mention "interviewing users" are really about discovering who to talk to, not about handing the conversation to a robot.
Only continue past this step once the user has confirmed they want the voice agent to conduct the interviews.
Before calling user-interview-topics-create, gather these:
interviewee_emails — list of email addressesinterviewee_distinct_ids — list of PostHog distinct IDstopic (required free text)agent_context (extra system prompt)questions listTopics snapshot their audience at create time — there is no live cohort link. If the user names a cohort, you (the agent) resolve cohort members to emails/distinct_ids before calling user-interview-topics-create. See Step 2 for the resolution flow and the 500-member cap UX.
The API rejects topics with no targeting, so interviewee_emails and/or interviewee_distinct_ids must end up non-empty.
If the request is vague, ask:
Skip these questions only when the user has already answered them.
Map what the user said to one of these paths:
cohorts-list (or a system.cohorts SQL search) to find the cohort, confirm the match, then resolve cohort members to emails/distinct_ids (see "Resolving a cohort" below).cohorts-create), then resolve it, or fall back to finding people by behavior (see below).cohorts-listpersons-list with a search queryEach email passes through DRF email validation (display-name format Paul D'Ambra <[email protected]> is accepted alongside plain [email protected]).
Topics snapshot their audience at create time. When the user picks a cohort, you must materialize the member list into interviewee_emails (and interviewee_distinct_ids for members without emails) before creating the topic.
Count the cohort first. Cheap query, decides the next step:
SELECT count() FROM persons WHERE id IN COHORT <cohort_id>
If the cohort has 500 or fewer members, fetch their emails:
SELECT properties.email AS email
FROM persons
WHERE id IN COHORT <cohort_id> AND properties.email IS NOT NULL
LIMIT 500
Put each row into interviewee_emails. Dedupe.
Cohort members without an email property aren't included by default — the persons.id column is the person UUID, not the SDK distinct_id, so it can't be used as an interviewee_distinct_id without a pdi.distinct_id join. If you specifically need to reach members who only exist as distinct IDs, ask the user first, then do the join explicitly.
If the cohort has more than 500 members, stop and ask the user. Do not silently truncate, sample, or fall back to a different cohort — the user needs to choose. Surface:
cohorts-create.ORDER BY rand() LIMIT <n> on the cohort query. Make the randomness explicit so they know they're not getting the "top" members.Pick the path with them, then re-run the resolution. Never proceed without an explicit decision.
Tell the user what you resolved. After resolution, confirm before creating: "Cohort 'X' has N members, resolved to E emails and D distinct IDs (snapshot — won't update if the cohort changes later)." This makes the snapshot semantics visible.
When the user describes who they want to talk to in behavioral terms, find them in the project's own data:
read-data-schema to list events that actually exist in the project. Don't guess event names from training data — PostHog event taxonomies are bespoke. Match the user's description to one or two candidate events; if multiple plausible matches exist, list them and ask which behavior they care about.execute-sql with HogQL. Filter by the chosen event over the last 60 days and group per person with coalesce(person.properties.email, distinct_id) AS id — this keeps both kinds of rows in a single query: people with an email group under it (directly usable as interviewee_emails), while emailless people fall back to their own distinct_id (for interviewee_distinct_ids) instead of collapsing into one junk None bucket that would sort to the top under ORDER BY count() DESC and eat a slot of the sample. The selected email column tells you which is which when routing (see below). The aggregates in each template (event_count, last_seen, days_since_last_seen) are what feed Step 5's per-interviewee context. Replace <event_name> with the chosen event:
SELECT coalesce(person.properties.email, distinct_id) AS id, person.properties.email AS email, count() AS event_count, max(timestamp) AS last_seen, dateDiff('day', max(timestamp), now()) AS days_since_last_seen FROM events WHERE event = '<event_name>' AND timestamp > now() - INTERVAL 60 DAY GROUP BY id, email HAVING count() >= 5 ORDER BY count() DESC LIMIT 20SELECT coalesce(person.properties.email, distinct_id) AS id, person.properties.email AS email, count() AS event_count, max(timestamp) AS last_seen, dateDiff('day', max(timestamp), now()) AS days_since_last_seen FROM events WHERE event = '<event_name>' AND timestamp > now() - INTERVAL 60 DAY GROUP BY id, email HAVING count() <= 2 AND dateDiff('day', max(timestamp), now()) > 14 ORDER BY count() ASC LIMIT 20SELECT coalesce(person.properties.email, distinct_id) AS id, person.properties.email AS email, count() AS event_count, max(timestamp) AS last_seen, dateDiff('day', max(timestamp), now()) AS days_since_last_seen FROM events WHERE event = '<event_name>' AND timestamp > now() - INTERVAL 60 DAY GROUP BY id, email HAVING count() >= 3 AND dateDiff('day', max(timestamp), now()) > 14 ORDER BY days_since_last_seen DESC LIMIT 20Route each row by its email column: rows where email is non-null go into interviewee_emails, and rows where email is null (so id holds the distinct_id) go into interviewee_distinct_ids — both can be set on the same topic. Keep event_count and days_since_last_seen per person so Step 5 can synthesise context like "used checkout 47 times in last 60 days; last seen 2 days ago".
topic is one or two sentences describing what the interview is about. Infer from context where possible — don't ask the user to repeat themselves.
Example: "ask trial users why they didn't convert" → topic: "Why trial users didn't convert in the first 14 days".
Two fields shape what the agent actually does on the call. Always ask about both before creating the topic.
questions is an ordered list the agent works through. Anchors, not a script — the agent will adapt phrasing. Keep them open-ended:
If the user already listed questions in their original request, use those and confirm. Otherwise, ask explicitly: "What questions do you want the agent to ask?"
If the user can't think of any, suggest 3–5 open-ended questions drawn from the topic and offer them for review before creating.
The field is technically optional in the API, but don't skip it silently — an interview with no questions is rarely useful.
Question templates by research goal:
agent_context is optional, but a few sentences here make the conversation dramatically better. Always offer the user the chance to provide it, e.g.:
"Want to give the agent any extra context? Things like tone, what to avoid, or background on the interviewee help guide the conversation. It's optional."
Useful kinds of context:
If the user declines, that's fine — leave agent_context empty and continue.
Once you have the pieces:
{
"topic": "Why trial users churned in week 2",
"interviewee_emails": ["[email protected]", "[email protected]"],
"interviewee_distinct_ids": ["distinct-id-with-no-email"],
"agent_context": "Be warm. The interviewee just churned — don't pitch.",
"questions": [
"What were you hoping PostHog would help with?",
"Where did you get stuck?",
"What would have made you stay?"
]
}
After creation, capture the returned topic ID — you'll need it for Step 5 and for handing off to the voice agent.
The topic-level agent_context applies to every interviewee. If the user knows something specific about individual interviewees that should shape that one conversation, attach it as a per-interviewee row via user-interview-topics-interviewees-create. This is optional — most topics won't need it.
Each row pairs an interviewee_identifier (must match one of the emails or distinct IDs in the parent topic's targeting) with an agent_context string. At most one row per (topic, interviewee). A user can have zero rows.
Good per-interviewee context looks like:
After Step 4 succeeds, ask the user: "Want to add per-interviewee context? Useful when individual people have very different backgrounds. You can either dictate the rows or paste a CSV."
If you found the audience via behavioral query in Step 2, you already have per-person context (usage counts, dormancy windows). Use it: e.g. "used checkout 47 times in last 60 days; last seen 2 days ago" for heavy users, "tried checkout once 18 days ago, never returned" for drop-offs.
If the user pastes a CSV, expect two columns: identifier,context. Either with or without a header row. Examples:
[email protected],uses replay but never summarization
[email protected],founder; very technical; skip product basics
Or with a header:
identifier,context
abc-distinct-id-1,churned from Scale last month — be empathetic
Parse the CSV, then create the rows with the captured topic_id. For more than a couple of rows, prefer user-interview-topics-interviewees-bulk-create — it takes an items array of (interviewee_identifier, agent_context) and creates up to 500 rows in one request, reporting inserted_count, skipped_count, and skipped_identifiers for pairs that already exist. Fall back to user-interview-topics-interviewees-create for a single row. Skip blank lines. Quote-escape commas inside the context cell — standard CSV rules.
If a row's identifier isn't present in the parent topic's interviewee_emails or interviewee_distinct_ids, warn the user before creating — the voice agent looks up context by exact string match, so a mismatched identifier just gets ignored at runtime.
interviewee_distinct_ids (the agent can still reach them via in-app delivery), or skip the behavioral query and let the user paste emails directly.read-data-schema returns multiple candidates (e.g. checkout_started, checkout_completed, checkout_abandoned), list them with counts and let the user pick the behavior they want to understand. Don't pick silently.UserInterview model (user_interviews_create with an audio file). Different flow, different model.user-interview-topics-list handles that directly with search, limit, and offset. No skill needed.UserInterview flow.data-ai
Signals scout for PostHog Tasks, the agent work items a project runs. Two lenses: delivery health (runs failing, clustered by repository and error class, and retry storms) every run, and on a slower rotation demand (recurring asks across human-authored tasks that point at a product gap). Skips the scout fleet's own run rows.
devops
Signals scout for the PostHog Conversations (support inbox) product. Watches the `$conversation_*` ticket-lifecycle events for support-delivery regressions — SLA breach-rate steps, first-response latency blowouts, backlog inflow-vs-resolution imbalance, and channel / assignment concentration — and files each dated regression as a report. Complements the per-ticket product-feedback signals the emission pipeline already fires; does not re-surface individual ticket content.
development
Populates and maintains a project's data catalog (semantic layer): canonical metrics, trust marks (certifications) on warehouse tables/views, and reviewed table relationships. Use when asked to set up / seed / bootstrap the data catalog or semantic layer, to catalog a project's metrics, to certify or deprecate data sources, to propose or review table joins, or to work through the proposal review queue. To *use* an existing catalog to answer a business-number question, see querying-posthog-data instead. Trigger terms: data catalog, semantic layer, canonical metric, certify table, deprecate source, relationship proposal, metric drift, review queue.
tools
Investigate logs in a PostHog project: verify a service or deployment is healthy, explain an error spike, triage an incident, or understand what a log stream is saying. Use when the user asks to "check the logs", asks whether a service, deploy, release, or change is working or broke anything, asks why errors are up or what changed, or wants the root cause of failures visible in logs. Routes the logs MCP tools (services overview, pattern mining, before/after pattern diffing, bucketed counts, facets, raw rows) so investigations start from summaries instead of raw rows or hand-written SQL over the logs table.