Validating and publishing canvases
SkillDev toolsValidate 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.
Available today. Use it from your connected AI after setup.
No other account needed.
Add ahel to your AI once: Claude, ChatGPT, Cursor, Claude Code or Codex. Then ask it to use this.
Then ask your AI: use the Validating and publishing canvases skill
What this skill tells your AI
The instructions your AI receives, as published by posthog/skills in skills/omnibus/validating-and-publishing-canvases/SKILL.md and read by ahel’s review.
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 isnullfor a canvas that has never been published — pass thatnullon 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 toph.loadInsight.capabilities.posthog.captureEvents— every event name it passes toph.capture.capabilities.posthog.inlineQueries: true— when it callsph.queryat all.capabilities.posthog.agentRequests: true— when it callsph.agent.request.capabilities.connectors— one{ "provider", "tools" }entry per third-party provider the canvas reads throughph.connectors.call, listing every tool it calls on that provider. A provider is a native id (github) ormcp:<server host>(mcp:mcp.calendly.com). Unknown providers, unregistered native tools, and private MCP hosts fail validation; every declared tool must haveis_read_only: truein 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 byfetch,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 likeintranet, and the.local,.localhost,.internal, and.home.arpasuffixes are all rejected, so a local dev host such ashttps://localhost:8010fails validation with aninvalid_network_originerror. 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:
errordiagnostics 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.warningdiagnostics don't block, but heed them:network_fetch/network_xhrmean the code reaches for the network directly. Declare the exact HTTPS origin or use thephbridge.
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-createwithoperations. 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-createwith the completeproject, for a first version or to replace everything.
For an edit with canvas-edit-create:
- Use
str_replacefor a change inside a file:old_stringis text copied from the file you read, with a few surrounding lines so it matches exactly one place. Setreplace_all: trueto change every match, for example a rename. - Use
writewith the completecontentfor a new file or a full rewrite,deleteto remove a file, andrenamewithnew_pathto move one. - When the change needs a new capability, for example a
ph.statescope, also sendcapabilitieswith the complete new capabilities (the current ones plus the addition) in the same edit. Do not switch tocanvas-publish-createfor 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_matchshows the closest lines of the file andedit_ambiguous_matchlists the lines that match. Fix those operations from the diagnostic and send the edit again. If one replacement fails twice,writethat 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 callcanvas-source-retrievebefore the next edit: you already know the source, and a 409version_conflicttells you when someone else changed it.
For a whole-project publish with canvas-publish-create:
- Always pass
expected_current_version_id— thecurrent_version_idyou read (or explicitnullon a first publish). Unguarded publishes can silently clobber concurrent edits. - Include a short
promptdescribing the change; it becomes the version-history entry's label. - Pass
nameonly 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'spublished_build_idadvances 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.
- Stage —
canvas-draft-createwith the completeproject(same shape, capabilities, and validation as a publish). Noexpected_current_version_id: a draft is based on nothing and conflicts with nothing. The response returns the draft'sversion_id, its queuedbuild, andcapability_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. - Wait for the build — poll
canvas-builds-retrieveuntil 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. - Preview — read the draft's files with
canvas-source-retrievepassing itsversion_id; once its build isreadythe app renders that draft when the version is opened. The draft is not incanvas-versions-retrieve(that lists published history only) and cannot be reverted onto — list pending drafts withcanvas-drafts-retrieve. - 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-createmakes the draft the live head. Passexpected_current_version_id(the livecurrent_version_idfromcanvas-source-retrieve); it is guarded exactly like a publish and 409s on a moved head (recover as below). A draft whose build is stillreadygoes 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:
- Re-read the source with
canvas-source-retrieve. - Re-apply your edits to the fresh source (the new head may contain someone else's changes — preserve them).
- 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.
Signals
- GitHub stars
- 71
- Forks
- 7
- Last commit
- Sep 2026
Advanced
- Item type
- skill
- Key
validating-and-publishing-canvases-2- Source
- github.com/posthog/skills