AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
This skill provides the workflow for building, editing, and maintaining standalone browser-based applications (canvases) within PostHog. It covers the entire development lifecycle, including resolving target canvases, selecting implementation approaches (React/Quill or HTML), managing state and data, and ensuring successful publication through validation and build monitoring. It distinguishes between freeform canvases and grid-based layouts, providing a structured approach to creating interactive data boards, documents, and tools.
What it does
- Resolves or creates PostHog freeform canvases based on user requests.
- Guides the selection of implementation stacks (React/Quill vs. HTML).
- Manages the read-edit-validate-publish-build lifecycle for canvas source code.
- Handles parameterization of canvas components using the editable() SDK.
- Integrates PostHog data, state, and actions via declared capabilities.
- Monitors build status and manages versioning for live canvases.
When to use it
- When a user requests a new data board, document, form, or small tool within PostHog.
- When editing or fixing an existing freeform canvas.
- When a task specifies a freeform canvas ID as the target for updates.
- When integrating PostHog data or interactive elements into a custom canvas interface.
Advantages
- Ensures consistent development practices for sandboxed PostHog applications.
- Provides a clear path for managing canvas state and user interactions.
- Automates the validation and build process to prevent broken deployments.
- Enables seamless integration with PostHog's data and insight ecosystem.
Usage examples
- “Create a new canvas for tracking our weekly signup funnel.”
- The agent will create a new freeform canvas, set up a React-based funnel component, configure the necessary PostHog insight capabilities, and publish the result.
- “Update the dashboard canvas in the marketing channel to include a new metric card.”
- The agent will retrieve the existing canvas source, add the requested metric component with appropriate parameters, validate the changes, and publish the update.
Install
Download the skill as a ZIP:
https://autogineer.com/agent-skills/posthog/building-canvases/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip building-canvases.zip -d .agents/skills
Documentation by PostHog
Building canvases
A canvas is a client-side browser application that runs in a sandboxed iframe inside PostHog.
Its source lives in PostHog — not in a repository — and you read and write it through the
canvas-* tools. Never write a canvas to a local file; publishing through the tool is what
saves it.
Canvas work can start from any ordinary task. A dedicated canvas mode or pre-created canvas is not required. When the user asks for a board, document, form, visualization, or small app that should live in PostHog, treat that as a canvas request and follow this skill.
This skill owns freeform canvases (standalone apps). Two other canvas kinds exist: grid
canvases (widget grids, including the user's home canvas) and component canvases (reusable
widgets grids place). When the target is a grid or home canvas, a placement, or a reusable
widget/component, load composing-grid-canvases instead — it owns the store search → configure →
fork → build ladder and the layout patch loop. Authoring a component's source still uses the
implementation companions below.
Resolve the target canvas
If the task names a canvas id (canvas-initiated tasks do), that is the target. Do not create another.
Otherwise the target channel is the one the task was created in — named in the task's context (the
channel_contextblock or the generation instructions). List that channel's canvases withcanvas-list(scope withchannel). If one is clearly what the request refers to — an earlier iteration of the same board or tool — build on it instead of creating a near-duplicate, and say so in your reply so the user knows where the result landed.Only when nothing existing fits, create one with
canvas-createin that same channel, named with a short descriptive title drawn from the request — never "Untitled canvas".Never survey channels to choose a target yourself: use
channel-listonly to resolve a channel the USER named to its id. Its listing puts the personal #me channel first, and #me is never a default — a canvas filed there is invisible to everyone else. If the task names neither a canvas nor a channel, ask which channel to use instead of guessing.
Load the companion skills for the implementation
This skill owns canvas selection and the authoring lifecycle. The companion skills hold the implementation contracts. Load every companion that applies before writing source:
building-react-quill-canvasesfor dashboards, data boards, forms, tools, application-like state, or anything that should look native to PostHog. It owns allowed imports, Quill composition, theming, charts, loading and error states, and the date picker.building-html-canvasesfor documents, articles, focused experiments, generative graphics,<canvas>, or WebGL work where application components add no useful structure. It owns semantic markup, direct browser APIs, animation cleanup, and non-Quill theming.querying-canvas-datawhenever the canvas reads PostHog data, captures events, or navigates. It owns thephSDK, saved-insight preference, result shapes, variables, date ranges, progressive per-query loading, and declared data capabilities. Load it alongside either implementation skill when data is involved.validating-and-publishing-canvasesfor every canvas. It owns project shape, capability declarations, validation diagnostics, guarded publishes, drafts, builds, and conflict recovery.
Mix implementation approaches when appropriate: React can own application chrome while browser graphics code owns a canvas element, or a mostly static page can mount one interactive island.
This is a judgment call, not a persisted mode — ask the user only when the choice changes a user-visible requirement you cannot infer.
Blocks
A canvas can contain premade blocks: numbers, goals, trends, top lists, funnels, retention, saved insights, live event feeds, SQL tables, filters, compare and refresh controls, and callouts.
They are React components in src/blocks/ that people drag into the canvas in the desktop app, so they are part of your source.
Read references/blocks.md before you change a canvas that has src/blocks/runtime.tsx.
Params
Expose the values a person may want to change as params. They can then change them in the desktop editor without a new task. Params are optional, but add them to each panel, card, or tool that has values worth changing: titles, copy, events, properties, thresholds, limits, colors. Params need React components. A plain HTML canvas has no params.
How the editor uses editable()
In the desktop editor, a person can select any element of the canvas: a heading, a paragraph, a section. They can move, duplicate, or delete it, and double-click text to change it. This works with no editable() call.
editable() makes a component one block. The editor then treats the block as one unit:
A click selects the full block, not the element inside it.
Move, duplicate, and delete act on the full block.
The side panel shows the block's params as fields. A change writes the new value into the block's props in the source.
To reach the elements inside a block, the person double-clicks the block to open it.
So use editable() on the parts a person thinks of as one thing and configures as one thing: a card, a chart, a table, a form. Keep the page layout around them as plain JSX. The person can then select a card as a block, and still select a page heading or section directly. If the component that holds the full page uses editable(), the full page becomes one block, and each click selects all of it.
Adding params
Put each panel in its own component. The component takes these values as props, with defaults.
Spread
editable(name, props, params)from@posthog/canvas-sdkon the root element of the component.nameis the component name.paramsmaps each prop name to its field.Use the component with literal props, for example
<SignupFunnel blockId="b-k3m9x2" title="Signup" windowDays={14} />. The editor writes changes into these props, so do not compute them inline.Give each use a unique
blockIdprop, for example<SignupFunnel blockId="b-k3m9x2" … />, so a person can point the agent at one card. Use only letters, digits,_,-,., and:, or the editor ignores the ID. In a list, build it from a stable key of the item:blockId={`kpi-${item.id}`}.
import { editable } from '@posthog/canvas-sdk'
export function SignupFunnel(props: { blockId?: string; title?: string; steps?: string[]; windowDays?: number }) {
const { title = 'Signup funnel', steps = ['$pageview', 'signed_up'], windowDays = 14 } = props
return (
<Card
{...editable('SignupFunnel', props, {
title: { type: 'text', label: 'Title', default: 'Signup funnel' },
steps: { type: 'events', label: 'Steps', default: ['$pageview', 'signed_up'] },
windowDays: { type: 'number', label: 'Conversion window (days)', min: 1, max: 90, default: 14 },
})}
>
…
</Card>
)
}Field types: text, longtext, number (min, max, step; with both min and max the editor shows a slider), boolean, select (options: strings or { value, label }), event, events, property, insight (a saved insight's short id), color. Each field can also have a description.
Give each field the same default as its prop default, for example { type: "boolean", label: "Show legend", default: true }. The editor shows default when the prop is not set.
A color param defaults to a theme token such as var(--primary), so the canvas follows light and dark mode until a person picks a hex in the editor.
Pass only JSON values (strings, numbers, booleans, string arrays) through params. Keep functions and query results out of them.
Images
Use public media library URLs for images in a canvas. Call posthog:media-images-list with
purpose="canvas" first and reuse a suitable image when one already exists.
To add a local image:
Call
posthog:media-image-upload-startwith the file name andpurpose="canvas".From a shell, POST the file to the returned
upload_urlas multipart form data. Include every returnedform_fieldsentry and put the file part last.Call
posthog:media-image-upload-completewith the returned id and use its permanenturlas the imagesrc.Add the URL's exact origin to
project.capabilities.network.origins. Canvas validation checks this declaration, and the published artifact uses it in its Content Security Policy.
Canvas media URLs are public and do not require authentication. Never upload secrets, credentials, customer data, or sensitive screenshots. Images must be under 4 MB and decode as PNG, JPEG, GIF, WebP, AVIF, or BMP. Never base64-encode image bytes into a tool call.
Common request patterns
Use these as routing examples, not fixed templates:
Product dashboard, web analytics board, or metric explorer: React + Quill plus data querying.
Checklist, form, or lightweight workflow: React + Quill, plus data querying for PostHog reads, event capture, or navigation. For a checklist or runbook specifically, start from the worked example in
building-react-quill-canvases(references/checklist-example.md) — team-shared progress via per-stepph.statekeys. Do not imply persistence that the available APIs do not provide.Document or narrative report: HTML for a mostly static reading experience; React + Quill plus data querying when it needs live PostHog data, filters, or application-like interactions.
Generative graphic or animation: HTML and browser graphics APIs. Add React only when it materially simplifies application state or chrome.
When the task carries a legacy requested pattern such as dashboard or web-analytics, apply the
matching shape above. The pattern is a hint; the user's actual request remains authoritative.
The iteration loop
Read the current source and version pointer with
canvas-source-retrieve. Remembercurrent_version_id— your publish must be guarded on it.Edit the project files using the implementation companions selected above. For any PostHog data the canvas shows, follow
querying-canvas-data(saved insights loaded via thephSDK — never fetch or your own PostHog client), make every figure verifiable — an insight-backed metric links its saved insight in PostHog, an ad-hoc query shows the exact query that ran, per that skill's "Verifiability" section — and declare everyphcall inproject.capabilities(insight short ids incapabilities.posthog.insights, captured events incaptureEvents,inlineQueries: truefor ad-hoc queries, andagentRequests: trueforph.agent.request) — the host enforces these at runtime and validation rejects undeclared calls.Follow
validating-and-publishing-canvases: validate withcanvas-validate-createas often as needed and fix every error-severity diagnostic.Save the project by publishing it — publishing is the default and goes live at once:
First version (
current_version_idis null): publish the complete project withcanvas-publish-create, passingexpected_current_version_id: null.Already live (
current_version_idis set): send only the edits withcanvas-edit-create, usingstr_replaceoperations for changes inside a file. Pass the livecurrent_version_idasexpected_current_version_id. Publish the complete project withcanvas-publish-createonly to replace the whole project.Stage a draft with
canvas-draft-createonly when the user asked for a draft, a preview, or a review step before going live. Follow thevalidating-and-publishing-canvasesskill for diagnostics and conflict recovery.
Wait for the build — drafts and publishes alike queue one. A publish or edit response already carries
build.build_status; use it when it isreadyorfailed. Otherwise pollcanvas-builds-retrieve(every few seconds, up to ~2 minutes) until your build isreadyorfailed. Onfailed, read the build's error diagnostics, fix the project, and save again — do not finish the task with a failed build.
Save once per requested change, when the canvas is ready — not after every micro-edit. When the
user asked for a draft, end your reply by saying a draft is ready to preview and promote; the
validating-and-publishing-canvases skill covers the draft → build → preview → promote flow.
End your reply by naming the channel the canvas is in and linking it with the url field the
canvas tools return (canvas-create, canvas-list, and the publish/source responses carry it).
That field is the only valid link to a canvas — never construct one yourself; guessed URLs
(project pages, web routes) do not resolve.
Runtime memory and actions
ph.state— durable key-value memory:ph.state.get(key, { scope }),ph.state.set(key, value, { scope })(a null value deletes the key),ph.state.list({ scope }). Scope"user"(the default) is private to each viewer;"shared"is one value per canvas, visible to the whole team. Declare the scopes you use incapabilities.posthog.state. Values are JSON, capped at 64 KB serialized and 256 keys per scope — store big data in PostHog (insights, the warehouse) and reference it. Never put secrets or viewer PII in state.ph.actions.invoke(verb, payload)— write into PostHog as the viewer. Declare every verb incapabilities.posthog.actions; undeclared or unregistered verbs fail validation and the host refuses them at runtime. Wire actions to explicit user gestures (a button the viewer clicks), never to load or render. The registry is the source of truth: list it with thecanvases-actions-retrievetool and follow each verb'susage(payload/result shape, behavior, and the confirmation copy it warrants) before wiring it.ph.tools.read(tool, args)andph.tools.call(tool, args): run an allowlisted PostHog MCP tool as the viewer, for example to read a feature flag and change its rollout. Declare every tool incapabilities.posthog.tools.readtakes read-only tools;callchanges data, so wire it to a click, and the host asks the viewer to confirm. Seequerying-canvas-datafor the allowlist and the version handling that flag writes need.ph.connectors.call(provider, tool, args)— read live third-party data (GitHub, or any MCP store server) with the VIEWER's own connection at view time. Never call GitHub, Calendly, or another service yourself and paste the result into the source: that snapshot is stale on publish and shows every viewer the author's data. Declare each provider and tool incapabilities.connectors; discover them with thecanvas-connectors-retrievetool. Seequerying-canvas-datafor the result and not-connected handling.ph.agent.request(prompt)— ask the canvas's authoring agent for a change, with the viewer's approval. DeclareagentRequests: trueincapabilities.posthog. Call it only from a direct click or form submission — the host shows the exact prompt and asks the viewer to accept before spending compute, and rejects calls made during render, mount, or polling. The agent publishes the change as a new version; a non-creator's request is filed in the authoring task's thread instead of starting a run.
Source-project shape
Keep
index.htmlas the entry shell returned by the source tool.src/canvas.tsxremains the conventional React entry component, but it may import additional relative TypeScript, TSX, JavaScript, JSON, SVG, CSS, and admitted asset files from the project.Self-contained module workers may be imported with
./worker.ts?worker. A worker must not import another local module.Use the public media library flow above for images. Other binary assets belong in the project's
assetsmap as base64 content with an admitted content type. WOFF/WOFF2, WebAssembly, and generic octet-stream assets are supported.Keep the platform dependency map exactly as returned. Do not add npm packages; local relative imports are project files, while bare imports remain limited to the platform-pinned set.
Files
- ATTRIBUTION.mdAdded by AutoGineer853 B
- LICENSELicence file added by AutoGineer1.0 KiB
- SKILL.md16.8 KiB
- references/blocks.md6.3 KiB
references/blocks.md
Canvas blocks (src/blocks/)
Blocks are premade React components that live in the canvas source.
People drag them into a canvas in the desktop app, and the app writes them into the code: the component file goes into src/blocks/, and a JSX line goes where the person dropped it.
They are ordinary code. Read them, import them, and change them like any other file.
When a canvas has blocks
The canvas has blocks when src/blocks/runtime.tsx exists.
Blocks that are already in the source show as JSX with a blockId prop, for example:
<Metric blockId="b-mf3k2a" title="Weekly active users" event="$pageview" math="weekly_active" />Rules
Keep every block that is in the source, with its
blockId, unless the person asks you to remove it.To add a block yourself, import it from
./blocks/<Name>and give it a new uniqueblockIdsuch asb-plus six letters or digits. Use only blocks whose file is insrc/blocks/. For a block that is not there, build your own component withuseCanvasFilters(); do not write block files orruntime.tsxfrom memory.All blocks share the canvas filters through
useCanvasFilters()andsetCanvasFilters()insrc/blocks/runtime.tsx. When you build your own chart or number, readuseCanvasFilters()too, so the date range and property filters apply to it.A block file is yours to change. Keep its exported name and its props, because the desktop reads them to edit the block.
src/blocks/library.jsonrecords which library version each block file is a copy of. Do not edit it. While a block file is unchanged, the desktop updates it to the newest library version; after you change a file, the desktop leaves it alone.Blocks call
ph.queryandph.statewith the user scope. KeepinlineQueries: trueand"user"incapabilities.posthog.state.An
Insightblock loads a saved insight withph.loadInsight. Put eachshortIdit uses incapabilities.posthog.insights. The desktop adds them when a person saves, but a publish of your own must list them.
Blocks
| Component | Props |
|---|---|
Metric | title, event, math (total, dau, weekly_active, monthly_active), format (number, percent, currency, duration), compare, followFilters |
Trend | title, events (string array), math, display (line, bar, area), breakdown, followFilters |
TopList | title, event, math, breakdown, limit, followFilters |
Funnel | title, steps (string array), windowDays, followFilters |
Retention | title, startEvent, returnEvent, period (Day, Week, Month), intervals, followFilters |
Insight | shortId (a saved insight), title (leave it out to use the insight's name) |
Goal | title, event, math, target, format, followFilters |
RecentEvents | title, limit, followFilters |
SqlTable | title, query (HogQL; put {filters} in the WHERE clause) |
Filters | label |
PropertyFilter | property, label |
DateRange | none |
Interval | none |
Compare | none. It draws the previous period on every Trend, as a dashed line. |
Refresh | none. It reloads every block and shows when the data last loaded. |
Callout | tone (info, success, warning), title, text |
Every data block also takes:
description: a short line under the title that tells readers what to take from the block.span:wideorfullmakes the block take two columns or the full row of its grid.
SQL mode
Metric, Goal, Trend, TopList, Funnel and RecentEvents also take a sql prop with HogQL. When sql is set, the block runs it instead of its builder settings. Retention and Insight have no SQL mode, and SqlTable takes its HogQL in query. Put {filters} in the WHERE clause so the canvas filters apply. The block reads the result by column position:
Metric: one row. The first column is the value, an optional second column is the previous value.Goal: one row with the value in the first column.Trend: the first column is the x axis. Each other column is a series, named by its column name.TopList: each row is a label, then a number.Funnel: each row is a step in order, the step name, then the count.RecentEvents: each row is a time, the event, then a detail.
Requirements and compatibility
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Security
No issues found by our static scan· No issues found
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- Published by
- PostHog
- Publisher
- PostHog (Official)
- Source
- PostHog/skills · skills/omnibus/building-canvases · commit a5d30e7
- Licence
- MIT License (redistribution allowed)
- LICENSE (added by AutoGineer from LICENSE)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
AutoGineer is not affiliated with or endorsed by PostHog or PostHog.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download building-canvases.zip