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_context block or the generation instructions). List that channel's canvases with
canvas-list (scope with channel). 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-create in 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-list only 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-canvases for 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-canvases for 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-data whenever the canvas reads PostHog data, captures events, or navigates.
It owns the ph SDK, 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-canvases for 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.
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-start with the file name and purpose="canvas".
- From a shell, POST the file to the returned
upload_url as multipart form data. Include every
returned form_fields entry and put the file part last.
- Call
posthog:media-image-upload-complete with the returned id and use its permanent url as
the image src.
- 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-step ph.state keys. 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.
Remember current_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 the ph SDK — 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 every ph call in project.capabilities (insight short ids in
capabilities.posthog.insights, captured events in captureEvents, inlineQueries: true for
ad-hoc queries, and agentRequests: true for ph.agent.request) — the host enforces these at
runtime and validation rejects undeclared calls.
- Follow
validating-and-publishing-canvases: validate with canvas-validate-create as often as
needed and fix every error-severity diagnostic.
- Save the project — which tool depends on whether the canvas is already live:
- First version (
current_version_id is null): publish the complete project with
canvas-publish-create, passing expected_current_version_id: null.
- Already live (
current_version_id is set): stage the complete project as a draft with
canvas-draft-create — the user previews the draft and promotes it to live. Publish or
promote yourself only when the user explicitly asked to make the change live.
Follow the validating-and-publishing-canvases skill for diagnostics and conflict recovery.
- Wait for the build — drafts and publishes alike queue one. Poll
canvas-builds-retrieve
(every few seconds, up to ~2 minutes) until your build is ready or failed. On failed,
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 you
staged 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 in capabilities.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
in capabilities.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 the
canvases-actions-retrieve tool and follow each verb's usage (payload/result shape,
behavior, and the confirmation copy it warrants) before wiring it.
ph.agent.request(prompt) — ask the canvas's authoring agent for a change, with the viewer's
approval. Declare agentRequests: true in capabilities.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 stages the
change as a draft for the canvas creator to review; a non-creator's request is filed in the
authoring task's thread instead of starting a run.
Source-project shape
- Keep
index.html as the entry shell returned by the source tool.
src/canvas.tsx remains 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
assets map 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.
1---
2name: building-canvases
3description: >
4 Create or edit a PostHog freeform canvas — a sandboxed browser application (data board, document,
5 form, small tool, graphics experiment) stored in PostHog and rendered by the desktop/web app. Use
6 when a task asks to build, generate, update, or fix a standalone canvas app, or when a freeform
7 canvas id is given as the publish target. For grid/home canvases, widget placements, or reusable
8 components, use composing-grid-canvases instead. Covers resolving or creating the target canvas,
9 choosing an implementation approach (React + Quill vs plain HTML/browser APIs), the read → edit →
10 validate → publish → build loop, and which companion canvas skills to load for the details.
11---
12
13# Building canvases
14
15A canvas is a client-side browser application that runs in a sandboxed iframe inside PostHog.
16Its source lives in PostHog — not in a repository — and you read and write it through the
17`canvas-*` tools. Never write a canvas to a local file; publishing through the tool is what
18saves it.
19
20Canvas work can start from any ordinary task. A dedicated canvas mode or pre-created canvas is
21not required. When the user asks for a board, document, form, visualization, or small app that
22should live in PostHog, treat that as a canvas request and follow this skill.
23
24This skill owns `freeform` canvases (standalone apps). Two other canvas kinds exist: `grid`
25canvases (widget grids, including the user's home canvas) and `component` canvases (reusable
26widgets grids place). When the target is a grid or home canvas, a placement, or a reusable
27widget/component, load `composing-grid-canvases` instead — it owns the store search → configure →
28fork → build ladder and the layout patch loop. Authoring a component's source still uses the
29implementation companions below.
30
31## Resolve the target canvas
32
33- If the task names a canvas id (canvas-initiated tasks do), that is the target. Do not create another.
34- Otherwise the target channel is the one the task was created in — named in the task's context
35 (the `channel_context` block or the generation instructions). List that channel's canvases with
36 `canvas-list` (scope with `channel`). If one is clearly what the request refers to — an earlier
37 iteration of the same board or tool — build on it instead of creating a near-duplicate, and say
38 so in your reply so the user knows where the result landed.
39- Only when nothing existing fits, create one with `canvas-create` in that same channel, named
40 with a short descriptive title drawn from the request — never "Untitled canvas".
41- Never survey channels to choose a target yourself: use `channel-list` only to resolve a channel
42 the USER named to its id. Its listing puts the personal #me channel first, and #me is never a
43 default — a canvas filed there is invisible to everyone else. If the task names neither a canvas
44 nor a channel, ask which channel to use instead of guessing.
45
46## Load the companion skills for the implementation
47
48This skill owns canvas selection and the authoring lifecycle. The companion skills hold the
49implementation contracts. Load every companion that applies before writing source:
50
51- **`building-react-quill-canvases`** for dashboards, data boards, forms, tools, application-like
52 state, or anything that should look native to PostHog. It owns allowed imports, Quill composition,
53 theming, charts, loading and error states, and the date picker.
54- **`building-html-canvases`** for documents, articles, focused experiments, generative graphics,
55 `<canvas>`, or WebGL work where application components add no useful structure. It owns semantic
56 markup, direct browser APIs, animation cleanup, and non-Quill theming.
57- **`querying-canvas-data`** whenever the canvas reads PostHog data, captures events, or navigates.
58 It owns the `ph` SDK, saved-insight preference, result shapes, variables, date ranges, progressive
59 per-query loading, and declared data capabilities. Load it alongside either implementation skill
60 when data is involved.
61- **`validating-and-publishing-canvases`** for every canvas. It owns project shape, capability
62 declarations, validation diagnostics, guarded publishes, drafts, builds, and conflict recovery.
63
64Mix implementation approaches when appropriate: React can own application chrome while browser
65graphics code owns a canvas element, or a mostly static page can mount one interactive island.
66
67This is a judgment call, not a persisted mode — ask the user only when the choice changes a
68user-visible requirement you cannot infer.
69
70## Images
71
72Use public media library URLs for images in a canvas. Call `posthog:media-images-list` with
73`purpose="canvas"` first and reuse a suitable image when one already exists.
74
75To add a local image:
76
771. Call `posthog:media-image-upload-start` with the file name and `purpose="canvas"`.
782. From a shell, POST the file to the returned `upload_url` as multipart form data. Include every
79 returned `form_fields` entry and put the file part last.
803. Call `posthog:media-image-upload-complete` with the returned id and use its permanent `url` as
81 the image `src`.
824. Add the URL's exact origin to `project.capabilities.network.origins`. Canvas validation checks
83 this declaration, and the published artifact uses it in its Content Security Policy.
84
85Canvas media URLs are public and do not require authentication. Never upload secrets, credentials,
86customer data, or sensitive screenshots. Images must be under 4 MB and decode as PNG, JPEG, GIF,
87WebP, AVIF, or BMP. Never base64-encode image bytes into a tool call.
88
89## Common request patterns
90
91Use these as routing examples, not fixed templates:
92
93- **Product dashboard, web analytics board, or metric explorer:** React + Quill plus data querying.
94- **Checklist, form, or lightweight workflow:** React + Quill, plus data querying for PostHog reads,
95 event capture, or navigation. For a checklist or runbook specifically, start from the worked
96 example in `building-react-quill-canvases` (`references/checklist-example.md`) — team-shared
97 progress via per-step `ph.state` keys. Do not imply persistence that the available APIs do not
98 provide.
99- **Document or narrative report:** HTML for a mostly static reading experience; React + Quill plus
100 data querying when it needs live PostHog data, filters, or application-like interactions.
101- **Generative graphic or animation:** HTML and browser graphics APIs. Add React only when it
102 materially simplifies application state or chrome.
103
104When the task carries a legacy requested pattern such as `dashboard` or `web-analytics`, apply the
105matching shape above. The pattern is a hint; the user's actual request remains authoritative.
106
107## The iteration loop
108
1091. Read the current source and version pointer with `canvas-source-retrieve`.
110 Remember `current_version_id` — your publish must be guarded on it.
1112. Edit the project files using the implementation companions selected above. For any PostHog data
112 the canvas shows, follow `querying-canvas-data` (saved insights loaded via the `ph` SDK — never
113 fetch or your own PostHog client), make every figure verifiable — an insight-backed metric
114 links its saved insight in PostHog, an ad-hoc query shows the exact query that ran, per that
115 skill's "Verifiability" section — and
116 **declare every `ph` call in `project.capabilities`** (insight short ids in
117 `capabilities.posthog.insights`, captured events in `captureEvents`, `inlineQueries: true` for
118 ad-hoc queries, and `agentRequests: true` for `ph.agent.request`) — the host enforces these at
119 runtime and validation rejects undeclared calls.
1203. Follow `validating-and-publishing-canvases`: validate with `canvas-validate-create` as often as
121 needed and fix every error-severity diagnostic.
1224. Save the project — which tool depends on whether the canvas is already live:
123 - **First version** (`current_version_id` is null): publish the complete project with
124 `canvas-publish-create`, passing `expected_current_version_id: null`.
125 - **Already live** (`current_version_id` is set): stage the complete project as a draft with
126 `canvas-draft-create` — the user previews the draft and promotes it to live. Publish or
127 promote yourself only when the user explicitly asked to make the change live.
128 Follow the `validating-and-publishing-canvases` skill for diagnostics and conflict recovery.
1295. **Wait for the build** — drafts and publishes alike queue one. Poll `canvas-builds-retrieve`
130 (every few seconds, up to ~2 minutes) until your build is `ready` or `failed`. On `failed`,
131 read the build's error diagnostics, fix the project, and save again — do not finish the
132 task with a failed build.
133
134Save once per requested change, when the canvas is ready — not after every micro-edit. When you
135staged a draft, end your reply by saying a draft is ready to preview and promote; the
136`validating-and-publishing-canvases` skill covers the draft → build → preview → promote flow.
137
138End your reply by naming the channel the canvas is in and linking it with the `url` field the
139canvas tools return (`canvas-create`, `canvas-list`, and the publish/source responses carry it).
140That field is the only valid link to a canvas — never construct one yourself; guessed URLs
141(project pages, web routes) do not resolve.
142
143## Runtime memory and actions
144
145- **`ph.state`** — durable key-value memory: `ph.state.get(key, { scope })`,
146 `ph.state.set(key, value, { scope })` (a null value deletes the key), `ph.state.list({ scope })`.
147 Scope `"user"` (the default) is private to each viewer; `"shared"` is one value per canvas,
148 visible to the whole team. Declare the scopes you use in `capabilities.posthog.state`.
149 Values are JSON, capped at 64 KB serialized and 256 keys per scope — store big data in
150 PostHog (insights, the warehouse) and reference it. Never put secrets or viewer PII in state.
151- **`ph.actions.invoke(verb, payload)`** — write into PostHog as the viewer. Declare every verb
152 in `capabilities.posthog.actions`; undeclared or unregistered verbs fail validation and the
153 host refuses them at runtime. Wire actions to explicit user gestures (a button the viewer
154 clicks), never to load or render. The registry is the source of truth: list it with the
155 `canvases-actions-retrieve` tool and follow each verb's `usage` (payload/result shape,
156 behavior, and the confirmation copy it warrants) before wiring it.
157
158- **`ph.agent.request(prompt)`** — ask the canvas's authoring agent for a change, with the viewer's
159 approval. Declare `agentRequests: true` in `capabilities.posthog`. Call it only from a direct
160 click or form submission — the host shows the exact prompt and asks the viewer to accept before
161 spending compute, and rejects calls made during render, mount, or polling. The agent stages the
162 change as a draft for the canvas creator to review; a non-creator's request is filed in the
163 authoring task's thread instead of starting a run.
164
165## Source-project shape
166
167- Keep `index.html` as the entry shell returned by the source tool.
168- `src/canvas.tsx` remains the conventional React entry component, but it may import additional
169 relative TypeScript, TSX, JavaScript, JSON, SVG, CSS, and admitted asset files from the project.
170- Self-contained module workers may be imported with `./worker.ts?worker`. A worker must not import
171 another local module.
172- Use the public media library flow above for images. Other binary assets belong in the project's
173 `assets` map as base64 content with an admitted content type. WOFF/WOFF2, WebAssembly, and generic
174 octet-stream assets are supported.
175- Keep the platform dependency map exactly as returned. Do not add npm packages; local relative
176 imports are project files, while bare imports remain limited to the platform-pinned set.