--- name: building-pigment-frames description: Use this skill whenever you're building, updating, or troubleshooting a Pigment Frame — a full-page, custom-coded visualisation (JavaScript, canvas, or inline SVG) that reads Pigment data through the PigmentSDK. Reach for it any time a native View (Grid, Chart, KPI) or Board can't produce what's being asked for - a bespoke chart, a custom canvas drawing, an interactive widget, or a standalone page in the left navigation that behaves like a small app. Also use it if a Frame you've already built shows up blank or broken and needs debugging. metadata: base_directory: /skills/building-pigment-frames includes: - "SKILL.md" --- # Building Pigment Frames A **Frame** is a standalone, full-page custom visualisation rendered from author-supplied JavaScript that reads Pigment data through the PigmentSDK. Use a Frame only when native Views, Boards, and chart types cannot express the required visual. ## When to Use - Bespoke visual (canvas, inline SVG, custom DOM) that no native View mode (`Grid` / `Chart` / `KPI`) can produce. No CDN; inline all code. - Full page in left navigation (like Boards). No Frame widget; a Frame *is* the page. Frames support Scenarios through the `ScenarioList` binding, see [Scenarios](#scenarios). Only the new `data_sources` / `data_sinks` path supports them, never the legacy `subscribeToVizualization`. ## Tools and Workflow The Frame JS lives in a file on your filesystem. `tool:create_frame` and `tool:update_frame` take the `path` of that file, never the source itself. | Tool | Use | | --- | --- | | `tool:write_file` | Author the JS file. The only way to get JS to `tool:create_frame` / `tool:update_frame`. | | `tool:edit_file` | Change the JS file in place. Prefer it over rewriting the whole file. | | `tool:create_frame` | Create once (name, `path`, bindings, data sources). Errors on name collision. | | `tool:update_frame` | Push the current file contents to an existing Frame; full replacement of `name`, `path`, `bindings`, `data_sources`. | | `tool:update_frame_body` | Exact, case-sensitive find-and-replace edits on the body already saved in Pigment. Keeps name, bindings, data sources and data sinks. All-or-nothing: fails if the old text is missing, or matches more than once without the replace-all flag. Returns edit counts, not the new body. It does not read or change your file. | | `tool:search_frames` | Look frames up. Pass `id` or `name` for one frame, which returns its full definition (`bindings`, `data_sources` and `data_sinks` included, ready for update). Pass neither to list ids + names; add `show_details: true` to get every body too. Empty result means no match. | 0. Identify the data (Metrics & Lists) that you Frames need to read from and write to. The Frame reads Metrics and List Properties directly through **data sources** that shape the rows (`labels`), columns (`values`) and pre-aggregation filters (`selectors`) it needs. The Frame writes back to Lists and Metrics directly through **data sinks** that target a List Item or Metric cell (`item` / `coordinates`) with the new property or cell values (`values`) it needs. Ask the user to confirm the read & write access they want to grant the Frame. 1. Before writing any code, ask the user if they have specific requirements regarding the design of the Frame: encourage them to share a color palette, screenshots, light / dark theme preferences. If the user gives their own design requirements, follow them and do not load `skill:designing-pigment-frames`. Otherwise, if `skill:designing-pigment-frames` is among your available skills (it is feature-flagged), load it and use it to create the Frame design. If it is not available, use a modern design with a light theme, clean typography, and a neutral color palette with a single accent color. 2. Declare the **bindings** (Metrics, Lists, List Properties, Scenarios) and one **data source** per dataset the Frame reads (see [Data sources](#data-sources)). Warnings: a) rows are capped at 1,000 per window ; b) concurrent subscriptions per Frame are limited to 10 data-source subscriptions and 20 List subscriptions. 3. `tool:write_file` the JS body to a file (e.g. `/frames/revenue-heatmap.js`; for a substantial Frame, write the skeleton first, see rule 7), then `tool:create_frame` once with that `path`. Never re-create (name collision). Use `tool:search_frames` with a `name` if unsure it exists. 4. For every later change, `tool:edit_file` the same file, then `tool:update_frame` with the same `path`. A Frame write reads the file at call time, so the file is the source of truth - keep editing it rather than rewriting it from scratch. Use `tool:update_frame_body` only for a small fix when you have no file for the Frame. 5. Make sure to save the changes that you made in the database using `tool:update_frame_body` or `tool:update_frame`. All changes that you make locally (in your context or file you write to) must be saved in the database using these tools to make sure the user can render it. 6. Always resend the complete `bindings`, `data_sources`, and `data_sinks` arrays when calling `tool:create_frame` / `tool:update_frame`; an omitted array drops every entry of that kind. Before `tool:update_frame`, read the current arrays with `tool:search_frames` by `id` or `name`, which returns all three; a bare listing returns none of them. 7. **Grow a large body in the file, not in tool arguments.** A Frame's JS body can be long enough that emitting it whole in one response risks hitting the model's max output size. For a substantial new Frame, `tool:write_file` a working skeleton (IIFE, `#app` setup, `__cleanup`, and clearly unique placeholder markers for the sections still to come — e.g. `// SECTION: subscribe`, `// SECTION: render`, `// SECTION: styles`), then fill each placeholder with `tool:edit_file`, one section at a time. Call `tool:create_frame` / `tool:update_frame` once the file is complete. Your earlier `tool:write_file` and `tool:edit_file` arguments may be removed from your context. Do not rely on remembering the file. To fill a section, use its marker line as old_string. To change text that is already filled, you may need to find it first. Use `tool:grep` with output_mode="content" and a short literal pattern, or `tool:read_file` with a small offset and limit. Prefer not to read the whole file if not needed to avoid filling up context. `tool:read_file` returns up to 500 lines by default, so prefer to set a small limit. A `tool:read_file` result is never evicted, so a big read stays in context. 8. If a Frame write reports the file was not found, the file was never written or the path is wrong: check with `tool:ls`, then `tool:write_file` before retrying. 9. Whenever the user reports a bug (blank page or anything that does not work expectedly), you can ask the user to record the logs using the dedicated buttons in the UI. You will be able to read the recorded logs. ## Publishing Frame writes are drafts. Only the user can publish. - `tool:create_frame`, `tool:update_frame` and `tool:update_frame_body` save to the user's private draft. You have no tool to publish or delete a Frame. - The user sees the draft in the Frame editor ("You are viewing a draft"). Everyone else sees the published version. A new Frame starts published and empty, so nobody else sees your work until the user publishes. - To publish, the user opens the Frame, selects **Publish** in the header, and confirms. They need edit rights on the Frame. **Discard** deletes the draft. - A write after a publish starts a new draft from the published version, and the user must publish again. A write before a publish edits the same draft. - The Frame name is not part of the draft: a rename is live for everyone at once. - After a publish, writes through data sinks do not ask the user to confirm, unless a user turns confirmation back on in the Frame's **Configure data inputs** setting. You cannot read or change this setting. When the Frame works, tell the user that it is still a draft and ask them to publish it. ## Sandboxed Runtime Iframe: `sandbox="allow-scripts"`, opaque origin. Host shell loads `/pigment-sdk.js`, then your file contents as a separate script into `#app`. **Allowed:** ES2015+ JS, DOM, Canvas, inline SVG/CSS. **Blocked:** network (`fetch`, CDN, `@import`), storage, `alert`/`confirm`/`prompt`, Workers, nested iframes, `parent`/`top`. **Rules:** 1. The file is pure JS (not HTML). Populate `#app`; no `