Omnibus
Skill 81 of 200
Implements a PostHog API (headless) survey inside the user’s own codebase so it renders with their UI components and captures the exact events PostHog expects, then verifies the…
3 minutes · 708 words · 5 sections
Install
npx skills add PostHog/skills --skill implementing-api-surveysnpx skills add PostHog/skills/plugin marketplace add PostHog/skillsThe first command installs just this skill, by the name in its SKILL.md; the second installs the whole repository.
An api survey is only a definition in PostHog. PostHog does not render it. The application renders the questions and captures the survey events. If the events or their properties are wrong, responses are missing, double-counted, or unattributed.
Fetch it with posthog:survey-get by id. Note each question’s id, type, question, choices, scale and optional, plus current_iteration. Never invent question ids.
If the survey does not exist yet, create it first with the creating-surveys skill (type api). Design guidance (short, rating or choice first, open text second) lives there too. Do not restate it here.
Before writing UI, find:
package.json or lockfile, or the server language’s manifest).conditions empty, and run the lookup below at the moment the survey should appear. getActiveMatchingSurveys only returns a survey with event conditions after that event fires, and nothing runs the lookup again at that point.Pick the path:
Require posthog-js 1.249.2 or newer. Older versions do not set the person properties and seen state on capture. Tell the user to upgrade rather than adding them by hand.
The lookup only works once both flags and the surveys bundle have loaded, because targeting is evaluated with flags and getActiveMatchingSurveys does nothing before surveys load. Register one retry for each at startup, and guard against rendering twice, since onFeatureFlags also runs on every flag reload (after identify, and after each response).
let triggered = false
let rendered = false
function tryShowSurvey() {
if (!triggered || rendered) return
posthog.getActiveMatchingSurveys((surveys) => {
const survey = surveys.find((s) => s.id === SURVEY_ID)
if (!survey || rendered) return
rendered = true
renderSurvey(survey)
})
}
// Register once at startup, not per trigger.
posthog.onFeatureFlags(tryShowSurvey)
posthog.onSurveysLoaded(tryShowSurvey)
function showSurveyIfEligible() {
triggered = true
tryShowSurvey()
}
// Call when the survey closes, after the final send or a dismiss.
function onSurveyClosed() {
triggered = false
rendered = false
}Call showSurveyIfEligible() from the app’s trigger, such as after a checkout completes, or right after setup if the survey belongs on page load. Call onSurveyClosed() when the survey closes, so a survey that can repeat shows again on the next trigger. A survey that cannot repeat stays hidden, because posthog-js marks it as seen. In a single-page app, register the two listeners where the app initializes PostHog, and call the functions they return to unsubscribe if that code can unmount. SURVEY_ID is the id from step 1. renderSurvey mounts the component you build from the app’s own UI, and that component calls the handlers below. getActiveMatchingSurveys applies the survey’s targeting for this person, so render only what it returns.
Capture three events. answers maps each question id to its answer, and holds every answer given so far in this submission. Exact property names and value shapes are in event-contract.md (opens in a new tab).
function onShown(survey) {
posthog.capture('survey shown', {
$survey_id: survey.id,
$survey_iteration: survey.current_iteration,
})
// posthog-js reads this key for the survey wait period; only its own renderer writes it.
try {
localStorage.setItem('lastSeenSurveyDate', new Date().toISOString())
} catch {}
}
function onSubmit(survey, answers, submissionId, completed) {
posthog.capture('survey sent', {
$survey_id: survey.id,
$survey_iteration: survey.current_iteration,
$survey_submission_id: submissionId,
$survey_completed: completed,
$survey_questions: survey.questions.map(({ id, question }) => ({ id, question })),
...Object.fromEntries(
Object.entries(answers).map(([questionId, value]) => [`$survey_response_${questionId}`, value])
),
})
}
function onDismiss(survey) {
posthog.capture('survey dismissed', {
$survey_id: survey.id,
$survey_iteration: survey.current_iteration,
})
}Rules that cause silent data problems:
$survey_submission_id (crypto.randomUUID()) when the respondent starts, and reuse it for every send of that submission. PostHog deduplicates and bills responses by this id. Events without it each count as a separate response.$survey_completed is false for a partial send and true for the final send.$survey_response_<question_id> key only for answered questions.survey dismissed after a final survey sent.posthog-js sets $survey_responded/<id>[/<iteration>] or $survey_dismissed/... and the local seen flag when it captures survey sent or survey dismissed. That is what stops the survey being offered again. Adding those properties yourself duplicates it.
Ask the user to trigger the survey once in the real app and submit it. Then:
posthog:surveys-responses-list for the survey id. Rows resolve question text server-side. Confirm the answer appears under the right question, and that multiple choice answers arrive as a list.posthog:survey-stats for the survey id. Confirm shown and sent counts moved, and that one submission is one response, not two.survey sent appears in the live events, then follow debugging-surveys. Do not guess.Report what you implemented, where the trigger lives, and the verified response.
Implements a PostHog API (headless) survey inside the user's own codebase so it renders with their UI components and captures the exact events PostHog expects, then verifies the first response arrived. Use when asked to implement a survey, build a headless survey, build a custom survey UI, render a survey in my app, or wire up a survey of type `api` in a browser, server, or custom mobile UI. Covers the posthog-js event contract, server-side SDK capture, and post-implementation verification through MCP. To design or create the survey definition, use creating-surveys instead. For a survey that is implemented but not showing or not recording, use debugging-surveys.
The verbatim description from this skill’s front matter — the string an agent matches on to decide whether to load it.
main, last pushed 8 November 2026.SKILL.md, not by matching a directory convention. 2 distinct layouts observed: skills/omnibus/*/SKILL.md, skills/posthog/all/skills/*/SKILL.md.h1 and no skipped levels:.claude-plugin/marketplace.json by PostHog, declaring 6 plugins. It is read for editorial metadata only — never as the skill index, which is always the repository tree./PostHog/skills.md, and each skill at its own .md URL.2 files · 7 KB
Everything this skill ships beside its prose. All of it is set here, as subchapters of skill 81.
Documentation the agent loads on demand, rather than up front.