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 (the building-canvases skill 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-list with kind=component and search=<what the widget shows>.
A component is placeable when both component_meta and published_build_id are set. If its
configSchema can express the request ("weather for Lisbon" → existing weather component with
config: {"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-create with kind=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=component and a description written 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 component key:
{
"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, no pattern — validation rejects them.
Design responsively. Give the component's root h-screen so it fills the placement iframe's
viewport, and 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 use h-full on the root: a published component's artifact
shell gives its html, body, and #root elements no explicit height, so height: 100%
collapses to the content height. Render usefully at minW×minH, and treat config as 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, anchored to its conversation task.
List them with the task comment tools (tasks-comments-list, tasks-comments-retrieve) on your task before and after changing the canvas, and address the open ones — a comment naming a broken widget is your brief for fixing it.
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 past grid.columns.
update_placement — merge changes into the placement with id. 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; generationTaskId links the task and prompt
records the ask. Set this when you start working on a placement from a task.
live — renders its component at version ("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 the prompt so 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 — config does not match the component's configSchema.
placement_size_out_of_contract — a warning, not an error: the box's w/h is 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.
1---2name: composing-grid-canvases3description: Compose PostHog grid canvases — widget grids (including the user's home canvas) built from reusable component canvases. Use when a task asks to add, fill, move, resize, or remove a widget on a grid or home canvas, to compose a whole canvas of widgets from one ask, to build a reusable widget/component, or when a placement id or grid canvas id is the target. Covers the component store search → configure → fork → build ladder, the component placement contract (size, configSchema), the placement lifecycle (pending/generating/live/failed), the guarded layout patch loop, and reading the canvas's comment threads.4---5
6# Composing grid canvases
7
8A grid canvas is a composition, not an app: a grid of placements, each rendering a **component
9canvas** (a reusable widget with its own source, build, and placement contract). The user's home
10canvas is an ordinary grid canvas in their personal channel. Layout is data — publishing or
11patching one is live immediately, with no build.
12
13Three canvas kinds share one lifecycle:
14
15- `freeform` — a standalone app (the `building-canvases` skill owns these).
16- `component` — a reusable widget. Same source/build pipeline as freeform, plus a placement
17 contract. Visibility rides its channel: personal channel = private, team channel = shared.
18- `grid` — a layout of placements referencing components. No file source; layout only.
19
20## The resolution ladder: configure, fork, build
21
22When a grid placement needs content ("a weather widget here", "a kanban of my tasks"), resolve in
23this order — placing an existing component beats authoring a duplicate:
24
251. **Search the store**: `canvas-list` with `kind=component` and `search=<what the widget shows>`.
26 A component is placeable when both `component_meta` and `published_build_id` are set. If its
27 `configSchema` can express the request ("weather for Lisbon" → existing weather component with
28 `config: {"location": "Lisbon"}`), place and configure it — write no code.
292. **Fork** when a component is close but its config cannot express the ask: read its source
30 (`canvas-source-retrieve`), create a new component (`canvas-create` with `kind=component`),
31 adapt, publish. Name the difference in the new component's description.
323. **Build new** when nothing fits — see "Building a component" below.
33
34New components land in the channel you create them in. Create them in the same channel as the grid
35they serve unless the user asks to share them more widely.
36
37## Building a component
38
39A component is authored exactly like a freeform canvas — load `building-react-quill-canvases` (or
40`building-html-canvases`) plus `querying-canvas-data` and `validating-and-publishing-canvases` —
41with three additions. A component that shows PostHog data follows the same verifiability rule as
42any canvas: an insight-backed figure links its saved insight, an ad-hoc query exposes the exact
43query that ran (see "Verifiability" in `querying-canvas-data`).
44Start from the complete, buildable project in [references/component-example.md](references/component-example.md); its envelope, placement contract, capability declarations, and defensive `ph.state` access are the parts that break when improvised.
45
46- **Create with `kind=component`** and a `description` written for store search: say what the
47 widget shows and what its config controls. Future placements are found by this text.
48- **Declare the placement contract** in the project's top-level `component` key:
49
50 ```json
51 {
52 "component": {
53 "size": { "defaultW": 2, "defaultH": 1, "minW": 1, "minH": 1, "maxW": 4 },
54 "configSchema": {
55 "type": "object",
56 "properties": { "location": { "type": "string", "description": "City to show weather for" } }
57 }
58 }
59 }
60 ```
61
62 Size is in grid units (widths 1–12, heights 1–40); `minW <= defaultW <= maxW`. The range is
63 advisory: users may resize a placement to any size, so it informs defaults and warnings, never
64 rejections. The config schema
65 vocabulary is an allowlist — `type`, `title`, `description`, `default`, `properties`, `required`,
66 `additionalProperties`, `items`, `enum`, `const`, `minimum`, `maximum`, `minLength`, `maxLength`,
67 `minItems`, `maxItems`, `format`. No `$ref`, no `pattern` — validation rejects them.
68
69- **Design responsively.** Give the component's root `h-screen` so it fills the placement iframe's
70 viewport, and adapt the layout to any size the user drags: a 2×1 placement is a glanceable tile;
71 a 6×4 is a full app surface. Do not use `h-full` on the root: a published component's artifact
72 shell gives its `html`, `body`, and `#root` elements no explicit height, so `height: 100%`
73 collapses to the content height. Render usefully at `minW`×`minH`, and treat `config` as the only
74 per-placement input.
75
76Publish and wait for the build like any canvas — a component with no ready build cannot go live on
77a grid.
78
79## Composing a whole canvas
80
81A whole-canvas ask ("a home canvas that summarizes my work in progress") usually means several widgets, not one.
82Plan the full set first — one placement per concern — then resolve each with the ladder above.
83Lay them out together: no overlaps, sizes matched to what each widget shows, the grid filled deliberately rather than tiles scattered in a corner.
84Batch 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.
85
86## Canvas comments
87
88Users leave feedback as comment threads on the canvas, anchored to its conversation task.
89List them with the task comment tools (`tasks-comments-list`, `tasks-comments-retrieve`) on your task before and after changing the canvas, and address the open ones — a comment naming a broken widget is your brief for fixing it.
90
91## Editing a grid
92
93The loop is read → patch, guarded exactly as `validating-and-publishing-canvases` describes for
94source publishes — with `canvas-layout-get` in place of `canvas-source-retrieve`, and
95`canvas-layout-patch` (surgical ops, guard required) or `canvas-layout-publish` (complete
96document, for an initial layout or full restructure) as the write. On a 409, re-read the layout,
97re-apply your change, and patch again.
98
99Operations:
100
101- `add_placement` — a new box: `{id, status, x, y, w, h, ...}`. Placements must not overlap or
102 extend past `grid.columns`.
103- `update_placement` — merge `changes` into the placement with `id`. Filling a drawn box is
104 `{"op": "update_placement", "id": "p1", "changes": {"status": "live", "component": "<component
105canvas id>", "config": {...}}}`.
106- `remove_placement`, `set_grid`.
107
108## The placement lifecycle
109
110A placement's `status` tells the renderer what to show:
111
112- `pending` — the user drew a box but hasn't described it (or the prompt awaits dispatch).
113- `generating` — an agent task is filling it; `generationTaskId` links the task and `prompt`
114 records the ask. Set this when you start working on a placement from a task.
115- `live` — renders its `component` at `version` (`"latest"` by default; a pinned version id is
116 allowed). Requires the component to be published and visible to the acting user.
117- `failed` — generation failed; keep the `prompt` so the user can retry or re-describe.
118
119When a task asks you to fill a placement, its prompt and the box's size are your brief: honor the
120drawn `w`×`h` and keep the placement's `prompt`
121intact for provenance.
122
123## Validation you will hit
124
125Layout publishes validate atomically; every error names its placement. The common ones:
126
127- `component_not_found` — the id is wrong, deleted, not a component, or not visible to the acting
128 user (a component in someone else's personal channel is not placeable).
129- `component_not_published` — the component has never published a placement contract.
130- `placement_config_invalid` — `config` does not match the component's `configSchema`.
131- `placement_size_out_of_contract` — a warning, not an error: the box's `w`/`h` is outside the
132 component's suggested range. The publish still succeeds; the component must render responsively.
133- `placements_overlap` / `invalid_placement` — geometry; fix coordinates rather than removing the
134 other widget.
135
136End your reply by linking the grid canvas with the `url` field the canvas tools return — never
137construct a canvas URL yourself.