PostHog is configured via a Public-API-driven sync script — not through the PostHog UI. Every dashboard, insight, and cohort the GTM motion depends on is a JSON file in this directory. The sync tool reconciles JSON ↔ PostHog. Git is the source of truth.
tools/posthog/
├── project.json # Nx project (posthog-tools)
├── env.ts # zod-validated env parsing
├── client.ts # openapi-fetch wrapper
├── schema.ts # zod schemas for local JSON
├── sync.ts # CLI: plan / apply / writeback
├── report.ts # CLI: pull insights → markdown
├── live-quality.ts # CLI: sample recent events and validate payload quality
├── *.spec.ts # tests
├── types/posthog-api.gen.ts # generated from PostHog OpenAPI spec
├── scripts/generate-types.ts # regenerate the above
├── dashboards/*.json # one JSON per dashboard
├── insights/*.json # reusable insight specs
└── cohorts/ # currently empty; populated post-1A
All commands wrap nx run posthog-tools:*. Root-package aliases:
npm run posthog:sync # → nx run posthog-tools:sync:plan
npm run posthog:apply # → nx run posthog-tools:sync:apply
npm run posthog:report # → nx run posthog-tools:report
npm run posthog:quality # → nx run posthog-tools:quality:live
npm run posthog:generate-types # → regenerate types/posthog-api.gen.tsDirect Nx invocations work too:
nx run posthog-tools:sync:plan
nx run posthog-tools:sync:apply
nx run posthog-tools:sync:apply --args="--delete-orphans"
nx run posthog-tools:quality:live -- --days 7 --limit-per-event 25
nx run posthog-tools:test
nx run posthog-tools:lintRequires a Personal API Key with dashboard:write, insight:write, cohort:write, project:read scopes. Create one at https://us.posthog.com/me/settings#personal-api-keys.
Env vars (see .env.example at repo root):
| Variable | Purpose |
|---|---|
POSTHOG_PERSONAL_API_KEY |
Write-scoped Personal API Key for local --apply and --report |
POSTHOG_PERSONAL_API_KEY_READONLY |
Read-only Personal API Key for CI --plan and live quality checks |
POSTHOG_HOST |
https://us.i.posthog.com (default) or your region |
POSTHOG_PROJECT_ID |
Numeric project id (visible in PostHog URL) |
CI maps POSTHOG_PERSONAL_API_KEY_READONLY into the tool's POSTHOG_PERSONAL_API_KEY environment variable for read-only --plan and live quality checks. Local development continues using the write-scoped POSTHOG_PERSONAL_API_KEY for --apply and --report.
// tools/posthog/insights/six-signal-activation-funnel.json
{
"slug": "six-signal-activation-funnel",
"posthog_id": null,
"kind": "funnel",
"window_minutes": 30,
"steps": [
{ "event": "cockpit:install_command_copied" },
{ "event": "cockpit:transport_connected" }
]
}Event names must match docs/gtm/taxonomy.md. The taxonomy.spec.ts test enforces this on every CI run.
telemetry-contract.ts is the machine-readable event/property contract used by tests and live checks.
taxonomy.spec.tsandtelemetry-contract.spec.tsguard committed dashboard JSON against undocumented events, unsupported breakdowns, unsupported filters, runtime dashboard coverage drift, and forbidden sensitive runtime fields.npm run posthog:quality -- --days 7 --limit-per-event 25samples recent live PostHog events and validates observed payloads against the same contract. It exits non-zero for missing required properties or forbidden sensitive properties, and prints warnings for non-contract fields.npm run posthog:quality -- --days 7 --limit-per-event 100 --require-critical-coveragealso requires recent samples for critical install and runtime events. The scheduledPostHog telemetry qualityworkflow runs this thresholded check daily and supports manual dispatch.- The live workflow requires Actions secrets named
POSTHOG_PERSONAL_API_KEYandPOSTHOG_PROJECT_ID.
--plan— diff against PostHog, no writes. Outputs[create] [update] [orphan]per artifact. CI runs this on every PR that affectsposthog-tools.--apply— idempotent upsert via PATCH. Re-running with no JSON change is a no-op (PostHog dedupes).--apply --delete-orphans— explicit deletion of remote artifacts that have no local JSON. Never automatic.posthog_idwriteback — first successful create writes the assigned PostHog id back into the JSON. Commit the writeback aschore(posthog): writeback ids for <slugs>.
To rename without losing the PostHog id:
- Edit the
slugfield in the JSON, keepingposthog_idunchanged. - Do not move the file — the file path is the slug source.
npm run posthog:syncwill detect this as an update, not a create + orphan.
PostHog publishes their full Public API as OpenAPI 3 at https://us.posthog.com/api/schema/. We commit the generated TypeScript types to avoid network calls at build time. Refresh quarterly:
npm run posthog:generate-typesReview the diff carefully — field renames in PostHog's API will surface here.
git blameanswers "who changed this metric and why."- No clicking through the PostHog UI ("api/cli-first" actually delivers).
- Reproducible on a fresh PostHog project for staging/test envs.
- Reviewable in PRs like any other change.
taxonomy.spec.tsprevents dashboards from referencing events the taxonomy doesn't document.
- gtm.md — durable strategy
- docs/gtm/taxonomy.md — event names
- cowork/gtm/SKILL.md — operates this CLI weekly