
Description
Get PostHog data into a canvas correctly: the host-injected `ph` SDK (loadInsight, query, capture, openExternal, navigate), the data hierarchy (saved insights first, typed query nodes second, inline HogQL last), per-insight-type result shapes, date-range wiring, and event capture from a canvas. Use whenever a canvas shows metrics, charts, tables, or any PostHog data, or needs to send analytics events.
SKILL.md
Querying canvas data
The global ph object (injected by the host — never imported, never initialized) is the only way
a canvas talks to PostHog. Credentials stay in the host; fetch(), posthog-js, and hand-rolled
clients fail in the sandbox.
Data hierarchy — back every metric with a saved insight
- Preferred — save an insight, load it by reference. Use the PostHog MCP insight tools to
create/save an insight that computes the metric with an insight query type (TrendsQuery,
FunnelsQuery, RetentionQuery, PathsQuery, or the web-analytics kinds WebOverviewQuery /
WebStatsTableQuery — not raw SQL). Confirm its numbers, note the
short_id, and render it withawait ph.loadInsight(shortId, { dateRange }). These are proven queries — numbers match the PostHog UI exactly (sessionization, unique users, breakdowns, bounce rate). Never fabricate a query or guess event/property names; discover and save them via MCP first. - Secondary — an ad-hoc typed node:
ph.query({ kind: "TrendsQuery", series: [...], dateRange: {...} })when saving an insight genuinely doesn't fit. - Last resort — inline HogQL:
ph.query("SELECT …"), only when no insight kind can express the metric; you then own the SQL and its date window.
For web-analytics boards specifically, use the web-analytics query kinds — raw HogQL subtly gets bounce rate, sessionization, channel attribution, and unique-visitor counts wrong.
Whatever tier you use, declare it in the project's capabilities before publishing: every
ph.loadInsight short id in capabilities.posthog.insights, every ph.capture event name in
captureEvents, and inlineQueries: true for any ph.query use. The host rejects undeclared
calls at runtime, and validation fails on undeclared literals.
Result shapes — read them correctly or every value renders 0
- Trends-style results (insight query types, via
ph.loadInsightor a typed node):resultsis an array of series objects, not rows. Each series hasdata: number[](per interval),days: string[](ISO),labels: string[],count(sum),aggregated_value(single-value total),label, and optionalcompare_label: "current" | "previous". A KPI total isresults[0].count(or.aggregated_value); a line chart plotsresults[0].dataoverresults[0].days. With a compare period, find the prior series bycompare_label === "previous"— never by index.columnsis empty here. - SQL results:
{ columns: string[], results: rows[][] }— each row an array of cell values incolumnsorder.
Load data in useEffect with useState, show a loading state, and aggregate in the query; never
fetch raw event dumps. Treat a rejected query and an empty result as different states: .catch
must set an error state that renders visibly (message + retry), never fall through to zeros, an
empty chart, or a "no data" message — a swallowed error makes real breakage (a missing table, an
auth failure) look like missing data. Reserve the empty state for a query that succeeded with no
rows.
Date windows
- Pass the canvas's date-picker window straight into
dateRange:ph.loadInsight(shortId, { dateRange: { date_from: win.start.toISOString(), date_to: win.end.toISOString() } })— the saved insight re-scopes to the window with no time SQL. Typed nodes take the samedateRange. Re-run every query when the window changes. - A saved SQL insight may ignore
dateRange(its window lives inside the SQL) — a reason to prefer insight query types. - Inline HogQL escape hatch only: never bake
now()or a hardcoded INTERVAL. Compute unix bounds (Math.floor(win.start.getTime() / 1000)) and write half-opentimestamp >= toDateTime(fromUnix) AND timestamp < toDateTime(toUnix). Prior period = the equal-length window immediately before; bucket withtoStartOfDay/toStartOfHour.
Side effects
ph.capture(event, properties?, distinctId?)— analytics events for interactions (fire-and-forget). Session replay,$session_id, and person attribution are handled by the host automatically; never roll your own capture.ph.openExternal(url)— openshttps://posthog.com/*.posthog.comURLs only, and only from a user interaction (opens outside focus are ignored). Don't link elsewhere.ph.navigate.toTask(id)/.toNewTask()/.toCanvas(id)/.toNewCanvas()— in-app navigation within the canvas's own channel.
More skills from the posthog repository
View all 74 skillsanalyzing-expensive-users
analyze expensive users in AI observability
Jul 28AnalyticsCost OptimizationObservabilityPostHogauditing-endpoints
audit PostHog project endpoints
Jun 8AnalyticsAuditPostHogauditing-warehouse-source-health
audit PostHog data warehouse source health
Jun 18AuditData WarehouseObservabilityPostHogauditing-warehouse-view-health
audit PostHog materialized view health
Jun 18AuditData WarehousePerformancePostHogauthoring-error-tracking-alerts
author PostHog error tracking alerts
Jun 18AlertingDebuggingObservabilityPostHogauthoring-log-alerts
author log alerts in PostHog
Jul 18AnalyticsMonitoringObservabilityOperations +1
More from PostHog
View publisherbuilding-canvases
create and edit PostHog canvases
posthog
Aug 6AutomationDesignPostHogPrototypingbuilding-html-canvases
author HTML and CSS PostHog canvases
posthog
Aug 6CSSDesignGraphicsHTML +1building-react-quill-canvases
build React and Quill canvases
posthog
Aug 6DesignFrontendReactUI Componentsbuilding-workflows
build and edit PostHog workflows
posthog
Aug 6AutomationMCPPostHogWorkflow Automationcheck-posthog-loading
inspect PostHog SDK loading across URLs
posthog
May 7AnalyticsDebuggingFrontendObservability +1consuming-endpoints-from-client-code
integrate PostHog endpoints into client applications
posthog
Jun 8API DevelopmentFrontendPostHogSDK