Composing Grid Canvases
PostHogOfficialPublished by PostHog
- Skill
- Workflow
- Data and AI
- MIT
- No issues found
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
This skill provides a structured approach to managing PostHog grid canvases, which are layouts of reusable widget components. It guides the user through the resolution ladder of searching for existing components, forking them for customization, or building new ones from scratch. It covers the full lifecycle of grid placements, including layout management, configuration schemas, and the guarded patch loop required for atomic updates to the canvas grid.
What it does
- Search and place existing components into grid canvases.
- Fork and adapt existing components when configuration needs exceed current capabilities.
- Build new reusable components with defined placement contracts and config schemas.
- Manage grid layouts through atomic patching and publishing operations.
- Handle the placement lifecycle from pending and generating to live or failed states.
- Integrate canvas comment threads into the development and maintenance workflow.
When to use it
- When you need to add, move, resize, or remove widgets on a PostHog grid or home canvas.
- When a task requires composing a full canvas of widgets from a single request.
- When you need to create a new reusable widget component for use across multiple grids.
- When addressing feedback or broken widgets identified in canvas comment threads.
Advantages
- Promotes reusability by prioritizing existing components over duplicate code.
- Ensures layout stability through guarded patch loops and atomic publishing.
- Provides a clear contract for component sizing and configuration.
- Integrates seamlessly with existing canvas lifecycle and comment management tools.
Usage examples
- “Add a weather widget to my home canvas showing Lisbon.”
- The agent searches the component store for a weather widget, configures it with the location set to Lisbon, and patches the home canvas layout to include the new placement.
- “Create a new widget that displays my recent task progress.”
- The agent builds a new component with a defined config schema, publishes it, and adds it to the specified grid canvas.
Install
Download the skill as a ZIP:
https://mp-staging.autogineer.com/agent-skills/posthog/composing-grid-canvases/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip composing-grid-canvases.zip -d .agents/skills
Documentation by PostHog
Composing grid canvases
A grid canvas is a composition, not an app: a grid of placements, each rendering a component canvas (a reusable widget with its own source, build, and placement contract). The user's home canvas is an ordinary grid canvas in their personal channel. Layout is data — publishing or patching one is live immediately, with no build.
Three canvas kinds share one lifecycle:
freeform— a standalone app (thebuilding-canvasesskill owns these).component— a reusable widget. Same source/build pipeline as freeform, plus a placement contract. Visibility rides its channel: personal channel = private, team channel = shared.grid— a layout of placements referencing components. No file source; layout only.
The resolution ladder: configure, fork, build
When a grid placement needs content ("a weather widget here", "a kanban of my tasks"), resolve in this order — placing an existing component beats authoring a duplicate:
Search the store:
canvas-listwithkind=componentandsearch=<what the widget shows>. A component is placeable when bothcomponent_metaandpublished_build_idare set. If itsconfigSchemacan express the request ("weather for Lisbon" → existing weather component withconfig: {"location": "Lisbon"}), place and configure it — write no code.Fork when a component is close but its config cannot express the ask: read its source (
canvas-source-retrieve), create a new component (canvas-createwithkind=component), adapt, publish. Name the difference in the new component's description.Build new when nothing fits — see "Building a component" below.
New components land in the channel you create them in. Create them in the same channel as the grid they serve unless the user asks to share them more widely.
Building a component
A component is authored exactly like a freeform canvas — load building-react-quill-canvases (or
building-html-canvases) plus querying-canvas-data and validating-and-publishing-canvases —
with three additions. A component that shows PostHog data follows the same verifiability rule as
any canvas: an insight-backed figure links its saved insight, an ad-hoc query exposes the exact
query that ran (see "Verifiability" in querying-canvas-data).
Start from the complete, buildable project in references/component-example.md; its envelope, placement contract, capability declarations, and defensive ph.state access are the parts that break when improvised.
Create with
kind=componentand adescriptionwritten for store search: say what the widget shows and what its config controls. Future placements are found by this text.Declare the placement contract in the project's top-level
componentkey:{ "component": { "size": { "defaultW": 2, "defaultH": 1, "minW": 1, "minH": 1, "maxW": 4 }, "configSchema": { "type": "object", "properties": { "location": { "type": "string", "description": "City to show weather for" } } } } }Size is in grid units (widths 1–12, heights 1–40);
minW <= defaultW <= maxW. The range is advisory: users may resize a placement to any size, so it informs defaults and warnings, never rejections. The config schema vocabulary is an allowlist —type,title,description,default,properties,required,additionalProperties,items,enum,const,minimum,maximum,minLength,maxLength,minItems,maxItems,format. No$ref, nopattern— validation rejects them.Design responsively. Give the component's root
min-h-screenso it fills the placement iframe's viewport and grows past it as content demands. Adapt the layout to any size the user drags: a 2×1 placement is a glanceable tile; a 6×4 is a full app surface. Do not useh-screenon the root: a fixed viewport height caps a flex column, so tall children shrink and clip instead of scrolling. Do not useh-fullon the root either: a published component's artifact shell gives itshtml,body, and#rootelements no explicit height, soheight: 100%collapses to the content height. Render usefully atminW×minH, and treatconfigas the only per-placement input.
Publish and wait for the build like any canvas — a component with no ready build cannot go live on a grid.
Composing a whole canvas
A whole-canvas ask ("a home canvas that summarizes my work in progress") usually means several widgets, not one. Plan the full set first — one placement per concern — then resolve each with the ladder above. Lay them out together: no overlaps, sizes matched to what each widget shows, the grid filled deliberately rather than tiles scattered in a corner. Batch the layout writes (one publish for an initial layout, surgical patches after) instead of one write per widget, and finish with every placement live or failed — never generating.
Canvas comments
Users leave feedback as comment threads on the canvas.
Run exec search ^canvas-comments-(list|retrieve)$ first.
If both tools are present, list the threads with them before and after changing the canvas, and address the open ones — a comment naming a broken widget is your brief for fixing it.
These tools return every thread on the canvas, including threads without a task. If either tool is absent, use the task comment tools as working-with-task-comments describes.
Editing a grid
The loop is read → patch, guarded exactly as validating-and-publishing-canvases describes for
source publishes — with canvas-layout-get in place of canvas-source-retrieve, and
canvas-layout-patch (surgical ops, guard required) or canvas-layout-publish (complete
document, for an initial layout or full restructure) as the write. On a 409, re-read the layout,
re-apply your change, and patch again.
Operations:
add_placement— a new box:{id, status, x, y, w, h, ...}. Placements must not overlap or extend pastgrid.columns.update_placement— mergechangesinto the placement withid. Filling a drawn box is{"op": "update_placement", "id": "p1", "changes": {"status": "live", "component": "<component canvas id>", "config": {...}}}.remove_placement,set_grid.
The placement lifecycle
A placement's status tells the renderer what to show:
pending— the user drew a box but hasn't described it (or the prompt awaits dispatch).generating— an agent task is filling it;generationTaskIdlinks the task andpromptrecords the ask. Set this when you start working on a placement from a task.live— renders itscomponentatversion("latest"by default; a pinned version id is allowed). Requires the component to be published and visible to the acting user.failed— generation failed; keep thepromptso the user can retry or re-describe.
When a task asks you to fill a placement, its prompt and the box's size are your brief: honor the
drawn w×h and keep the placement's prompt
intact for provenance.
Validation you will hit
Layout publishes validate atomically; every error names its placement. The common ones:
component_not_found— the id is wrong, deleted, not a component, or not visible to the acting user (a component in someone else's personal channel is not placeable).component_not_published— the component has never published a placement contract.placement_config_invalid—configdoes not match the component'sconfigSchema.placement_size_out_of_contract— a warning, not an error: the box'sw/his outside the component's suggested range. The publish still succeeds; the component must render responsively.placements_overlap/invalid_placement— geometry; fix coordinates rather than removing the other widget.
End your reply by linking the grid canvas with the url field the canvas tools return — never
construct a canvas URL yourself.
Files
- ATTRIBUTION.mdAdded by AutoGineer865 B
- LICENSELicence file added by AutoGineer1.0 KiB
- SKILL.md8.5 KiB
- references/component-example.md6.5 KiB
references/component-example.md
A known-good component
This is a complete, buildable component project — the welcome checklist PostHog seeds onto new home canvases (products/canvas/backend/welcome.py, verified against the real canvas builder).
The envelope below shows the fields you author; its index.html and dependencies come from canvas-source-retrieve and are kept exactly as returned (see validating-and-publishing-canvases) — the placeholders below stand in for them.
Start from it when building a checklist, settings, or any state-carrying widget, and keep its structure even when you replace the content: the project envelope, the placement contract, the capability declarations, and the defensive ph.state access are the parts that break when improvised.
The project envelope
{
"schemaVersion": 1,
"entryHtml": "index.html",
"files": {
"index.html": "<synthetic shell from canvas-source-retrieve — loads /src/canvas.tsx as a module>",
"src/canvas.tsx": "<the component below>"
},
"dependencies": {
"react": "<pinned>",
"react-dom": "<pinned>",
"@posthog/quill": "<pinned>",
"lucide-react": "<pinned>"
},
"capabilities": {
"posthog": { "insights": [], "inlineQueries": false, "captureEvents": [], "state": ["user"], "actions": [] },
"network": { "origins": [] }
},
"component": { "size": { "defaultW": 3, "defaultH": 5, "minW": 2, "minH": 3 } }
}index.htmlmust load/src/canvas.tsxas a module and every imported package must be independencies— the retrieved shell and dependency map already satisfy both. Improvising a comment-only entry fails the build withno_entry_module; dropping the@posthog/quillorlucide-reactentries fails it withimport_not_declared.Declare the full
capabilities.posthogshape (insights,inlineQueries,captureEvents,state,actions) even when a field is empty, as this example does — the build freezes the declared capabilities into the artifact manifest, which older clients read expecting every field.insights,inlineQueries, andcaptureEventsare also required by the API: a partialposthogobject is rejected with a 400 before source validation runs. Populate only what the code calls and leave the rest empty.capabilities.posthog.statemust name every scope the code passes toph.state.*— validation rejects an undeclared scope.component.sizeis in grid units and advisory; the component still has to render at any size the user drags.
The component (src/canvas.tsx)
import { useEffect, useState } from 'react'
import { Checkbox, SkeletonText, Text, Tooltip, TooltipContent, TooltipTrigger } from '@posthog/quill'
import { Info } from 'lucide-react'
const ITEMS = [
{ id: 'download-desktop', label: 'Download PostHog Desktop', hint: null },
{
id: 'add-widget',
label: 'Add a widget to this canvas',
hint: 'Select Edit in the top right, then click and drag anywhere on the dotted grid and describe what should go there.',
},
{
id: 'start-task',
label: 'Start your first task',
hint: 'Open a space and tell the agent what you want done. It picks the task up and reports back.',
},
]
// Persistence needs both the SDK surface and a host that answers it. An old
// runtime without ph.state must degrade to session-only ticks, never crash
// the tile.
const stateApi = typeof ph !== 'undefined' && ph.state && typeof ph.state.get === 'function' ? ph.state : null
export default function WelcomeChecklist() {
const [checked, setChecked] = useState(null)
useEffect(() => {
let cancelled = false
if (!stateApi) {
setChecked({ 'download-desktop': true })
} else {
stateApi
.get('checked', { scope: 'user' })
.then((value) => {
if (!cancelled) {
setChecked(value && typeof value === 'object' ? value : {})
}
})
.catch(() => {
if (!cancelled) {
setChecked({})
}
})
}
return () => {
cancelled = true
}
}, [])
const toggle = (id, value) => {
if (!checked) {
return
}
const next = { ...checked, [id]: value }
setChecked(next)
if (stateApi) {
stateApi.set('checked', next, { scope: 'user' }).catch(() => {})
}
}
const done = checked ? ITEMS.filter((item) => checked[item.id]).length : 0
return (
// The component root fills the placement iframe viewport and grows past it as content demands.
<div className="flex min-h-screen flex-col gap-2 p-3">
<div className="flex items-baseline justify-between gap-2">
<Text weight="medium">Welcome to PostHog</Text>
{checked ? (
<Text size="sm" className="text-muted-foreground">
{done} of {ITEMS.length} done
</Text>
) : null}
</div>
{checked === null ? (
<SkeletonText lines={5} className="text-sm" />
) : (
<div className="flex flex-col gap-1.5">
{ITEMS.map((item) => (
<div key={item.id} className="flex items-center gap-2">
<Checkbox checked={!!checked[item.id]} onCheckedChange={(value) => toggle(item.id, value === true)} />
<Text size="sm" className={checked[item.id] ? 'text-muted-foreground line-through' : ''}>
{item.label}
</Text>
{item.hint ? (
<Tooltip>
<TooltipTrigger render={<Info size={13} className="shrink-0 text-muted-foreground" />} />
<TooltipContent>
<div className="max-w-60">{item.hint}</div>
</TooltipContent>
</Tooltip>
) : null}
</div>
))}
</div>
)}
</div>
)
}The rules the example encodes
One file, one
export defaultReact component, no props, nocreateRoot— the host mounts it.Imports only from the platform allowlist (
react,@posthog/quill,lucide-react,recharts,dayjs).phis a host-injected global — never import it, and feature-detect optional surfaces likeph.stateso the widget degrades instead of crashing on an older runtime.Loading state renders a skeleton, never a blank; every async access has a
.catchthat lands in a renderable state.Fill the placement iframe viewport with
min-h-screenand let the iframe document scroll.h-screencaps the flex column, so tall children shrink and clip;h-fullcannot resolve on the root because the published artifact shell has no explicit height.
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/composing-grid-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.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download composing-grid-canvases.zip