Skip to main content

validating-and-publishing-canvases

Validate and publish a canvas source project safely: the source-project shape, declared capabilities, reading the current version pointer, iterating on validation diagnostics, guarded publishing with expected_current_version_id, staging a draft build and promoting it, waiting out the queued build, and recovering from a 409 version_conflict or a 429 capacity limit without overwriting concurrent work. Use whenever a canvas edit is ready to save, a draft build is wanted, a canvas publish or build returns diagnostics or a conflict, or a task needs to understand canvas version history.

Source facts

Repository
PostHog/ai-plugin
Last source activity
October 5, 2026 at 12:50
Detected SKILL.md language
English
Stars
88
Forks
18

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
validating-and-publishing-canvases
description
Validate and publish a canvas source project safely: the source-project shape, declared capabilities, reading the current version pointer, iterating on validation diagnostics, guarded publishing with expected_current_version_id, staging a draft build and promoting it, waiting out the queued build, and recovering from a 409 version_conflict or a 429 capacity limit without overwriting concurrent work. Use whenever a canvas edit is ready to save, a draft build is wanted, a canvas publish or build returns diagnostics or a conflict, or a task needs to understand canvas version history.
# Validating and publishing canvases A canvas's source lives in PostHog, versioned per publish. Publishing is guarded: every edit is based on a specific version, and the server refuses to overwrite newer work. Every publish queues a server-side build, and the canvas renders the last successful build. ## The source project `canvas-source-retrieve` returns: - `project` — `schemaVersion` (1), `files` (path → content), `entryHtml` (`"index.html"`), `dependencies` (exact platform-pinned versions), `canvasSdkVersion`, `capabilities`. - `current_version_id` — the version your edits are based on. Keep it; the publish needs it. It is `null` for a canvas that has never been published — pass that `null` on the first publish. Keep `index.html` and `dependencies` exactly as returned. You may add relative source files and admitted assets to the project. Use `?worker` for a self-contained module worker and represent binary assets as base64 entries in `assets`; new npm dependencies or dependency-version drift fail validation. ## Declare capabilities The host enforces `project.capabilities` at runtime, so an undeclared `ph` call builds fine and then dies in the rendered canvas. Declare: - `capabilities.posthog.insights` — every insight short id the canvas passes to `ph.loadInsight`. - `capabilities.posthog.captureEvents` — every event name it passes to `ph.capture`. - `capabilities.posthog.inlineQueries: true` — when it calls `ph.query` at all. - `capabilities.posthog.agentRequests: true` — when it calls `ph.agent.request`. - `capabilities.connectors` — one `{ "provider", "tools" }` entry per third-party provider the canvas reads through `ph.connectors.call`, listing every tool it calls on that provider. A provider is a native id (`github`) or `mcp:<server host>` (`mcp:mcp.calendly.com`). Unknown providers, unregistered native tools, and private MCP hosts fail validation; every declared tool must have `is_read_only: true` in the catalog. An upstream hint alone does not grant access. A canvas with connectors cannot declare shared state. - `capabilities.network.origins` — each exact HTTPS origin used by `fetch`, `XMLHttpRequest`, or an external stylesheet, image, font, media file, or frame. Remote scripts remain blocked. Do not include paths, credentials, queries, fragments, or wildcards. The host must be public: loopback and private IPs, single-label names like `intranet`, and the `.local`, `.localhost`, `.internal`, and `.home.arpa` suffixes are all rejected, so a local dev host such as `https://localhost:8010` fails validation with an `invalid_network_origin` error. Data sent to a declared origin leaves PostHog and appears in the capability review before promotion. Before validation, inventory every literal external URL in every source file. Classify navigation links and `ph.openExternal()` URLs as navigation; they do not need a network origin. For every request or resource URL, declare its scheme + host + optional port only. Include every origin a request redirects to and every secondary origin a stylesheet references for fonts or images. Never infer that one CDN hostname covers another. Validation rejects undeclared literal calls and resource URLs (`capability_missing_*` diagnostics) so you can fix them before publishing. Dynamic URLs and redirect destinations cannot be inferred, so the inventory is still required even when validation is clean. ## Validate until clean `canvas-validate-create` is side-effect free; call it as often as needed. Diagnostics carry `severity`, a stable `code`, a `message`, and (for file-specific problems) `path` and `line`: - `error` diagnostics block publishing — fix all of them. Common ones: `import_not_allowed` (bare imports are limited to the dependencies returned in the source project), `forbidden_dynamic_import` / `forbidden_require` / `forbidden_inline_script`, `invalid_path`, `capability_missing_insight` / `capability_missing_capture_event` / `capability_missing_inline_queries` / `capability_missing_agent_requests` / `capability_missing_network_origin`, `dependency_not_admitted` / `dependency_version_mismatch`, `platform_token_redeclared` (a CSS variable named like a Quill token that the platform stylesheet sets on every element, so the value never applies; prefix your own variables), and path/size violations. - `warning` diagnostics don't block, but heed them: `network_fetch` / `network_xhr` mean the code reaches for the network directly. Declare the exact HTTPS origin or use the `ph` bridge. ## Publish guarded Publishing goes live immediately and is the default way to save a change, for a canvas's first version and for every follow-up edit. Every version records who published it and which task did the work, so the history stays reviewable after the fact. Stage a draft instead only when the user asked for a draft, a preview, or a review step — see "Draft, then promote" below. Two ways to publish, both guarded: - **Edits** — `canvas-edit-create` with `operations`. This is the default for any change to a canvas that already has source. The guard is mandatory here because an edit's meaning depends on its base. - **Whole project** — `canvas-publish-create` with the complete `project`, for a first version or to replace everything. For an edit with `canvas-edit-create`: - Use `str_replace` for a change inside a file: `old_string` is text copied from the file you read, with a few surrounding lines so it matches exactly one place. Set `replace_all: true` to change every match, for example a rename. - Use `write` with the complete `content` for a new file or a full rewrite, `delete` to remove a file, and `rename` with `new_path` to move one. - When the change needs a new capability, for example a `ph.state` scope, also send `capabilities` with the complete new capabilities (the current ones plus the addition) in the same edit. Do not switch to `canvas-publish-create` for that. - Put every operation of one change into one call. They apply in order, and the whole edit is rejected if any operation fails. - A 400 lists each failed operation by index. `edit_no_match` shows the closest lines of the file and `edit_ambiguous_match` lists the lines that match. Fix those operations from the diagnostic and send the edit again. If one replacement fails twice, `write` that whole file instead. - The response returns the new `current_version_id`. Pass it to the next edit. When you published or edited the canvas earlier in this session, do not call `canvas-source-retrieve` before the next edit: you already know the source, and a 409 `version_conflict` tells you when the canvas changed. For a whole-project publish with `canvas-publish-create`: - Always pass `expected_current_version_id` — the `current_version_id` you read (or explicit `null` on a first publish). Unguarded publishes can silently clobber concurrent edits. - Include a short `prompt` describing the change; it becomes the version-history entry's label. - Pass `name` only to rename the canvas (e.g. a first build of an untitled canvas). - Publish once per requested change. If the user asks for another edit afterwards, re-read the source (the head may have moved) and publish again — don't batch unrelated changes into one version, and don't publish work-in-progress after every micro-edit. - A 429 means the team's build capacity is temporarily exhausted. Wait ~30 seconds and retry the same publish; nothing was saved. The response returns the new `current_version_id`. ## After publishing: wait for the build A publish queues a server-side build of the version. **The canvas does not update until the build is ready, and nobody else is watching the result — you own it.** The publish or edit response already carries `build.build_status`. When it is `ready` or `failed`, act on it without another call. Only while it is `queued` or `building`, poll `canvas-builds-retrieve` every few seconds (up to ~2 minutes) until the build you queued is terminal: - `queued`/`building` — in progress; poll again shortly. - `ready` — the canvas's `published_build_id` advances to this build (unless a newer publish superseded it first). The task's canvas work is done. - `failed` — read the build's error diagnostics, fix the project, and publish again. A failed build never replaces the last good one, so the canvas keeps rendering the previous version — finishing the task here would leave the user with a stale canvas and a silent failure. Runtime error reports (filed on the authoring task when a rendered canvas throws) name the build they came from. A report from an **older build id** is history, not evidence about your current code — check it against the build you just published before acting on it. In particular, a report that a documented `ph` API is undefined (e.g. `ph.state`) means that artifact was baked by an older host runtime: republish so a current build replaces it. Never "fix" it by removing the API or its capability declaration. ## Draft, then promote Publishing goes live the moment its build is ready, and that is the default. Use a draft only when the user asked for one: a preview to look at first, a review step before going live, or an explicit "don't publish yet". A draft is a real, buildable version that is never the head: the live canvas keeps rendering the current version until someone promotes the draft. This is different from `canvas-validate-create`, which only compile-checks and produces no build or preview. 1. **Stage** — `canvas-draft-create` with the complete `project` (same shape, capabilities, and validation as a publish). No `expected_current_version_id`: a draft is based on nothing and conflicts with nothing. The response returns the draft's `version_id`, its queued `build`, and `capability_widening` — the insights, capture events, inline queries, and network origins the draft declares beyond the live version. Surface a non-empty widening to the user before promoting; it is the access the change would newly grant. 2. **Wait for the build** — poll `canvas-builds-retrieve` until the draft's build is terminal, the same way you would after a publish. A failed draft build is fixed by staging a new draft, not by promoting. 3. **Preview** — read the draft's files with `canvas-source-retrieve` passing its `version_id`; once its build is `ready` the app renders that draft when the version is opened. The draft is **not** in `canvas-versions-retrieve` (that lists published history only) and cannot be reverted onto — list pending drafts with `canvas-drafts-retrieve`. 4. **Promote** — when the user approved the draft or asked to go live; a draft the user asked to review stays staged until they say so. `canvas-promote-create` makes the draft the live head. Pass `expected_current_version_id` (the live `current_version_id` from `canvas-source-retrieve`); it is guarded exactly like a publish and 409s on a moved head (recover as below). A draft whose build is still `ready` goes live with no rebuild; otherwise a fresh build is queued, so wait for it as in step 2. Promote is the only path from a draft to live. ## Recovering from 409 version_conflict A 409 means the canvas moved past your base — a concurrent publish or a revert. The response includes the live `current_version_id`. Never retry unguarded to force your version through: 1. Re-read the source with `canvas-source-retrieve`. 2. Re-apply your edits to the fresh source (the new head may contain the user's own edits — preserve them, and do not report them in your reply). 3. Publish again with the new `current_version_id`. ## Version history semantics Each publish appends a full source version and moves the head pointer; users can revert to older versions in the app (which republishes and rebuilds them). The guard matters because basing your publish on the version you actually read is what keeps a user's revert, another agent's publish, and your edit from silently erasing each other.
View on GitHub