← SupersimpleCONTENT HISTORY
WHAT CHANGED
Update to Supersimple
Snapshot Oct 24, 2026 · 13:36 UTC · version 0.1.0
Collection source: downloaded plugin package.
First saved snapshot
No earlier snapshot is available to establish a change.
Save Supersimple and check future changes in Following.
Up to 10 plugins free · no card required.
Compare saved observations
Download comparison JSONFull technical diff · 0 changed fields
Full snapshot data
{
"description": "Build and publish Supersimple Apps inside explorations. Use for custom app blocks, visualizations and widgets, the Supersimple SDK, or supersimple app commands.",
"included_files": [
{
"relative_path": "references/connected-backends.md",
"size_in_bytes": 1655
},
{
"relative_path": "references/raw-sql.md",
"size_in_bytes": 1270
}
],
"name": "supersimple-apps",
"skill_md_contents": "---\nname: supersimple-apps\ndescription: Build and publish Supersimple Apps inside explorations. Use for custom app blocks, visualizations and widgets, the Supersimple SDK, or supersimple app commands.\nmetadata:\n minimum-cli-version: \"2.27.0\"\n---\n\n# Supersimple Apps\n\nA Supersimple App is client-side web code (usually a single HTML file) published into a\nSupersimple workspace. It renders inside a sandboxed iframe as a \"Custom app\" block in explorations,\nand accesses data through the **Supersimple SDK**, exposed as the global `sup` object.\nThe SDK uses the viewer's Supersimple session and permissions. Semantic-layer queries\napply the models' row-level access rules; raw SQL runs directly against warehouse tables.\n\nThe [Supersimple MCP server](https://docs.supersimple.io/integrations/mcp-server) lets your\ncoding agent inspect live explorations and App source. Use an existing MCP connection when available;\notherwise add `https://app.supersimple.io/mcp` to your coding\nagent's MCP connections and authenticate. Use streamable HTTP and OAuth; see the guide for\nclient setup and API-token authentication. This connection is optional for CLI-based app\ndevelopment, but required for the MCP workflows below.\n\n## Before building\n\nCLI steps require terminal access and separate CLI authentication; otherwise use available MCP tools or prepare files for the user to apply.\n\nCLI authentication, the coding agent's Supersimple MCP connection, and the browser viewer's\nsession are separate. Confirm they point to the intended workspace; their permissions can differ.\nIf CLI credentials are missing, have the user run `supersimple login` with a token from\n**Settings → Developers**, or use an existing `SUPERSIMPLE_TOKEN`. Never put that token in App code.\n\nFor an existing App, use its local source project if available. Otherwise read its source and\nmanifest with MCP `get_app` before editing; pass the block's `pinnedVersionId` as `versionId`\nwhen present. Preserve its backend declarations and state keys. Decide whether the user wants\na change to this block or a published update affecting other blocks before publishing.\n\nChoose the data path:\n\n- Existing exploration output: use `sup.exploration.fetchBlock` and subscribe to invalidations.\n- New analysis: inspect model fields and metrics, then use `sup.query`; do not invent model IDs\n or replace live data with hardcoded samples unless the user asks for a mockup.\n- Raw warehouse SQL: read [raw SQL](references/raw-sql.md) for its permission and model-rule differences.\n- External persistence or actions: read [connected backends](references/connected-backends.md).\n They require workspace registration and backend-side authorization, not just an SDK call.\n\n## Workflow\n\n```bash\nsupersimple app create my-app # scaffold my-app/ (app.yaml + index.html)\ncd my-app\nsupersimple app publish # publish/update in the workspace (versions append)\n```\n\nRequire CLI ≥2.27.0 (`supersimple --version`). Check `supersimple status` output, including the\naccount and host; it always exits 0, even on failure. `SUPERSIMPLE_HOST` optionally overrides\nthe API host. The account comes from the token.\nUse `supersimple app <command> --help` for command options. If the CLI is missing, see\nhttps://docs.supersimple.io/guides/managing-the-data-model/using-the-cli.\n\nIf product behavior differs from this skill, compare `supersimple --version` with the latest\nreleased CLI and check for skill updates using the\n[skill installation guide](https://docs.supersimple.io/guides/ai-coding-agents#installation).\nThe minimum CLI version above is not a freshness check. Update the complete skill folder,\nincluding its references, preserving local edits, then retry the affected step. If the mismatch\nremains, report the observed behavior rather than assuming the skill is correct.\n\nCLI 2.27.0–2.27.1 scaffolds an `AGENTS.md` pointing to an older, CLI-embedded skill.\n`supersimple app skill --install` in those versions overwrites this guide with that older copy;\nupdate this separately installed skill through the installer you used to obtain it instead.\n\n**The dev loop is publish + reload the page** in a test exploration, exercising the actual sandbox, CSP,\nand upstream inputs. New apps are personal: only their author sees them in the picker, but other\nviewers can use them through shared explorations after consent. `supersimple app promote <name>`\nadds an app to the team library; `demote` removes it. These actions need the corresponding workspace\npermissions (**Manage Apps** for publish/delete/promote/demote).\n\nChoose a distinctive, namespaced manifest name. `supersimple app list` shows only team apps and\nyour own personal apps, so it cannot rule out name collisions. Publishing the same name\nupdates the existing app (including a team app or another author's app) and can restore a soft-deleted\none. Use a distinct name when developing a separate app. `publish --dir <directory>` selects the app\nfolder without changing directories. `supersimple app delete <name>` deletes an app; use only when\nremoval is explicitly requested.\n\nMCP `draft_app` creates a source-only draft of an **existing** App; it cannot create a new App or\nchange its manifest. It returns an `appVersionId` but does not edit an exploration or attach the\ndraft to a block. Use it only with a workflow that can set the target block's `appVersionId`\n(`app_version_id` in exploration YAML). Until then, the block still runs its previous version.\n\n## app.yaml\n\n```yaml\napp:\n name: my-app # unique per workspace; stable identifier (users see the title)\n title: My app # human-readable; used as the default block title\n description: >- # one line describing what the app does\n Ranks accounts by expansion potential.\n entry: index.html # published as-is; keep the app self-contained in this one file\n tags: [ops] # optional; the first one shows as a chip in the picker\n```\n\nThe CLI validates the manifest when publishing; unknown keys are rejected.\n\n## Supersimple SDK\n\nAvailable through the global `sup` object only inside a Supersimple App iframe; no SDK import\nis required. All methods are async. The examples below are API fragments, not a complete app.\n\n```js\nconst ctx = await sup.context(); // { user: {id, email}, account: {id}, app: {id} }\nconst theme = await sup.theme(); // viewer appearance tokens, e.g. theme.brandMain, theme.textDefault, theme.background\n\n// Query the semantic layer as the viewer. `pipeline` is an ARRAY of operations with\n// camelCase parameter keys, not an exported YAML pipeline object. Empty array = base rows.\nconst result = await sup.query({\n baseModelId: '<model id>',\n pipeline: [],\n sort: [{ key: 'created_at', direction: 'DESC' }], // engine-side ordering — what a sortable header means\n first: 500,\n});\n// result: { rows: object[], fields: [{key, type, name}], hasMore: boolean, endCursor?: string }\n// Grouped rows nest values under $children; see \"Named metrics and grouped results\" below.\n// Page with the cursor: pass endCursor as `after` on the next call, keeping baseModelId,\n// pipeline, sort, and variables identical between pages. Default page size: 100; max: 10,000.\n// Aggregation property is an object, e.g. {type: \"sum\", key: \"amount\",\n// property: {key: \"total\", name: \"Total\"}}. deriveField uses `fieldName`:\n// { operation: 'deriveField', parameters: { key: 'year', fieldName: 'Year',\n// value: { expression: 'year(created_at)', version: '1' } } }\n\n// Data-producing blocks in the surrounding exploration.\nconst blocks = await sup.exploration.getBlocks();\n// [{ blockId, title, kind, properties: [{key, type, name}], aggregated }] — runs no queries.\n// Match the columns this App needs. This chart example needs a numeric output.\n// Aggregated blocks are valid chart inputs; exclude them only when you need per-record rows.\nconst usable = blocks.filter((b) => b.properties.some((p) => p.type === 'Number'));\n\n// Fetch a block's output table. Use the user-requested block or a saved selection;\n// auto-select only when exactly one block fits. Store block IDs, not titles or array indexes.\nif (usable.length === 0) {\n // Show a setup message rather than dereferencing a missing block.\n} else if (usable.length > 1) {\n // Show an input picker unless the requested/saved block already resolves.\n} else {\n const upstream = await sup.exploration.fetchBlock(usable[0].blockId);\n // { rows, fields, hasMore }; hasMore means truncated, not a complete dataset.\n}\nconst block = await sup.exploration.getBlock('known-block-id'); // metadata or null\n\n// In the finished app, register this once before the initial fetch, then refetch on changes.\n// Upstream changes do not reload the iframe.\nawait sup.exploration.onBlockInvalidated(({ blockId }) => { // a fetched block changed underneath you\n if (blockId === myBlockId) refetchAndRender(); // (steps edited, or a variable feeding it moved)\n});\nawait sup.exploration.onBlocksChange((blocks) => rebuildPicker(blocks)); // list changed: add/remove/rename\n\n// Exploration variables: read, write (drives native blocks too), subscribe.\nconst vars = await sup.variables.get();\nconst declared = await sup.variables.list(); // [{ key, kind }] — find e.g. kind 'date_range'\n// From a user action: await sup.variables.set('segment', 'enterprise');\nawait sup.variables.onChange((next) => render(next));\n// Better than reading a variable's value: reference it in your pipeline and the engine\n// resolves it exactly like a native filter (dynamic presets and timezone included):\n// { operation: 'filter', parameters: { key: 'created_at', operator: '==',\n// value: { expression: 'variable(\"range\")', version: '1' } } }\n\n// Persisted app state: part of the exploration's state, so it survives reload, travels in a\n// shared link, and browser-back steps over it like any other exploration change.\nconst state = await sup.state.get();\nawait sup.state.onChange((next) => applyState(next)); // undo/redo, links, echoes of own writes\n// From a user action: await sup.state.set({ x: 'mrr' });\n```\n\nDo not pass `variables` to `sup.query`: that array (`[{type,key,value}]`) replaces exploration\nvariables. Reference `variable(\"range\")` in the pipeline instead.\n\nDiscover model IDs with `supersimple models list` and inspect fields with\n`supersimple export --model-ids <id> --stdout` outside the sandbox. Prefer\n`sup.exploration.fetchBlock(...)` over ad-hoc `sup.query` when the app is meant\nto sit downstream of no-code steps — it keeps the data prep auditable. `fetchBlock` consumes the\nblock's output, which can be aggregated; it does not accept cursor/pagination parameters. Handle\n`hasMore` as truncation and ask for a narrower upstream query when completeness matters. Subscribe\nto invalidations to make the app reactive. For a direct `sup.query` that uses variables, subscribe\nto variable changes and rerun it. Guard against older async responses overwriting newer results.\n\n### Named metrics and grouped results\n\nSDK parameter names use camelCase: a named-metric aggregation uses `metricId`.\nCLI YAML uses `metric_id`. Keep user-defined field keys exactly as declared on the model.\n\n```js\nconst revenueBySegment = await sup.query({\n baseModelId: '<model id>',\n pipeline: [{\n operation: 'groupAggregate',\n parameters: {\n groups: [{ key: 'segment' }],\n aggregations: [{\n type: 'metric',\n metricId: '<named metric id>',\n property: { key: 'revenue', name: 'Revenue' },\n }],\n },\n }],\n first: 100,\n});\n```\n\n`sup.query` preserves the API's grouped row structure. Each grouping dimension adds a level:\nits value is on the parent, and `$children` contains the next level or the aggregate values.\n`$leaves` counts leaf result rows under that parent in the returned page, not source records\nor a metric total. For example, `revenueBySegment.rows` can be:\n\n```js\n[\n { segment: 'enterprise', $leaves: 1, $children: [{ revenue: 12500 }] },\n { segment: 'self-serve', $leaves: 1, $children: [{ revenue: 800 }] },\n]\n```\n\nFlatten recursively for a chart or table, carrying parent dimensions into every leaf:\n\n```js\nfunction flattenRows(rows, parent = {}) {\n return rows.flatMap(({ $children, $leaves, ...values }) => {\n const row = { ...parent, ...values };\n return Array.isArray($children) ? flattenRows($children, row) : [row];\n });\n}\nconst revenueRows = flattenRows(revenueBySegment.rows);\n// [{ segment: 'enterprise', revenue: 12500 }, { segment: 'self-serve', revenue: 800 }]\n```\n\nThis handles multiple grouping dimensions and flat rows. Flatten each fetched page and append\nits leaves; a group may span pages. Use `hasMore` and `endCursor` to determine completeness.\n`sup.exploration.fetchBlock(...)` already flattens grouped output and returns flat rows.\n\nCheck the actual response before mapping it into a chart. A missing aggregate key is a shape\nerror; do not turn it into zero with `row.revenue ?? 0`. Preserve genuine zero and null values\nand verify a known nonzero result against the source exploration.\n\n## Appearance and charts\n\nThe iframe does **not** inherit the host's stylesheet or CSS variables. `sup.theme()` returns\ncolor values; apply them to your own styles and chart-library options. It does not render charts,\nlegends, or a color picker. Do not assume additional SDK helpers exist.\n\n```js\nconst theme = await sup.theme();\nfor (const [key, value] of Object.entries(theme)) {\n document.documentElement.style.setProperty(`--app-${key}`, value);\n}\n```\n\nUse those locally defined variables, for example `color: var(--app-textDefault)` and\n`background: var(--app-surface2)` on a chart panel, `textMuted` for secondary labels, `border`\nfor boundaries, and `surface1` for tooltips. Set body margins, font, and form-control colors\nexplicitly; browser defaults do not follow the viewer's Supersimple appearance. Theme changes\nreload the iframe, so apply tokens again on startup. Test the exploration URL with\n`?theme=light` and `?theme=dark`, regardless of the operating system's theme.\n\nUnless the user asks otherwise, chart Apps should follow these product conventions:\n\n- Use a compact chart panel and avoid repeating the host block's title inside it.\n- Put a wrapping legend below the plot. Keep category colors stable across filtering and sorting;\n the native categorical sequence starts with `blue40`, `purple40`, `teal40`, `red40`, `orange40`,\n `green40` from `sup.theme()`. Use the same colors in marks, legend, and tooltips.\n- Keep totals outside a pie, or inside a sufficiently large donut hole; never overlay them on\n colored slices. Format units and precision for readability, with exact values in tooltips.\n- Give the plot a bounded height and responsive width; reserve space for labels and the legend.\n Check narrow blocks and long labels, not just a full-width view.\n\n## Runtime constraints\n\n- **Size to content, never to viewport.** The sandbox reports its content height and the block hugs\n it. Use intrinsic heights; avoid `100vh`/`100dvh` (causes sizing feedback). Users can also switch\n the block to \"Fill available space\". Natural height is clamped to 120–1600 px.\n- **console.log works.** Sandbox console output and uncaught errors appear on the page's own\n console prefixed `[app:<app title>]` — use it to debug instead of guessing.\n- **No secrets in app code.** Everything in the bundle is visible to every viewer. There is no\n server side. Use public/publishable keys and the viewer token from `sup.auth.getToken`\n for declared backends. Never use backend service-role credentials.\n- **Dependencies:** import ESM from esm.sh (e.g. `import * as d3 from 'https://esm.sh/d3@7'`),\n or bundle locally into a self-contained HTML document. The CLI uploads only `entry` as text;\n it does not build code, upload sibling files, or wrap a bare JavaScript bundle in HTML.\n Pin tested dependency versions for reproducibility. `esm.sh` is permitted for scripts,\n not arbitrary fetches; other network calls need a declared, registered backend.\n- **Sandbox:** scripts only; no localStorage/cookies, popups or new-tab links, form submissions,\n alert/confirm dialogs, remote images, or eval. Images must use `data:` or `blob:`. WASM is allowed.\n esm.sh is not allowed for stylesheets.\n- **Versioning:** every publish appends a numbered version. Unpinned blocks follow the published\n version after a full page reload; the block menu’s \"Reload app\" only remounts the cached bundle.\n Blocks with `app_version_id` keep their pinned version.\n\n## State: reproduce the user's view\n\n`sup.state` stores this block's view configuration in the exploration, not private per-user\npreferences. Restore meaningful choices on reload, shared links, and back/forward navigation.\n\n| What the value represents | Where it belongs |\n| --- | --- |\n| Selected input block, metric, chart type, grouping, sort, or App-specific filter | `sup.state` |\n| Selected record, tab, or drill-down that defines the view a shared link should open | `sup.state`; store stable IDs or keys |\n| A parameter that should also drive native exploration blocks | `sup.variables`; use the existing variable definition |\n| Hover, tooltip, loading/error status, request bookkeeping, or input the user has not applied | App memory |\n| Fetched rows or values calculated from the data and current choices | App memory; refetch or recompute as needed |\n\nRead saved state on startup and subscribe once to `sup.state.onChange`. Treat incoming state as\nan instruction to restore the view, including undo/redo and returning to an earlier selection.\nUse exploration variables directly when they own a setting; duplicating them in `sup.state`\ncreates competing values.\n\nWrite small patches from deliberate user interactions. A completed selection, applied filter,\nor finished drag is a useful history step; keep in-progress typing and dragging local until\ncommitted. Group related choices into one patch so undo restores a coherent view. Skip writes\nwhen the persisted values are already the same.\n\nDefaults and fallbacks are computed at render time. An empty saved state must render a useful\ninitial view without writing it back. If a saved block or column disappears, show a fallback or\nsetup message and preserve the saved choice until the user chooses a replacement. Rendering,\nfetch completion, and `onChange` handlers must not save derived or repaired state. A Reset action\ncan explicitly save the default choices because the user requested it.\n\n`set` shallow-merges a patch, replacing whole nested values; send the complete nested value you\nintend to retain. The host coalesces writes over roughly 400 ms and caps serialized state at\n8,192 characters. Store compact choices rather than data or whole block definitions. Catch\nrejected `sup.state.set` promises and show the save error: the SDK updates its local state and\nfires `onChange` before the host accepts the write. A fulfilled call acknowledges the host request;\nthe exploration update follows the host's debounce. Subscriptions have no unsubscribe handle.\n\n## Debug data with Supersimple MCP\n\nWhen an App's numbers disagree with an existing exploration, use the **Supersimple MCP server's**\ntools below. If no Supersimple MCP connection is configured, ask the user to connect and authenticate it\nusing the setup guide above; installing this skill alone does not provide these tools.\n\n1. `get_exploration` locates the App and its input blocks. `get_exploration_block` returns each\n block's definition and complete `dereferencedPipeline`; for an App block it also identifies\n the mounted App and persisted state. Use `get_app` to inspect its source, passing `versionId`\n for a pinned version.\n2. `get_exploration_block_data` runs the source data block. Pass matching variable values in\n `params` (otherwise defaults apply), and use `limit`/`offset` for a small comparison sample.\n Match the account and user permissions as well as filters and date range.\n3. Compare a known nonzero value with the App's result. A wrong source result points toward the\n model, pipeline, or inputs; a correct source result narrows the investigation to the App's\n query, result handling, or rendering.\n\nMCP returns a formatted table for an existing data block. Inspect the actual `sup.query`\nresponse in the running App to debug JavaScript, `$children` nesting, or a new SDK query.\nExploration reads require `data:read`; `get_app` requires `apps:read`; drafting and publishing\nrequire `apps:write`. Publishing also needs **Manage Apps**. If a connection exists but a tool\nis missing, check its granted scopes and user role before asking the user to reconnect.\n\n## Review and verify\n\nTest in the running Supersimple exploration. Local HTML preview and a successful publish do not\nvalidate the `sup` bridge or sandbox. Exercise the interactions the App actually supports:\n\n- Change an upstream filter or variable and verify a specific expected value, not just a redraw.\n- Change an input or other saved choice; reload or reopen the link, then use back/undo and\n forward/redo. The controls and result must agree. Loading or restoring state must not add history.\n- Check empty, error, and truncated results; light/dark appearance; and narrow block sizing.\n Inspect the parent console for `[app:<app title>]` errors.\n\n## Using the published app\n\nIn any exploration: **Templates → Custom app** inserts a block; pick the app in the right sidebar\n(\"Custom app\" section). The sidebar has the app picker, \"Fill available space\", and \"Show block\ntitle\". The block title defaults to the app title; reactivity is implemented with subscriptions.\n"
}SHA-256 of public snapshot: 05567e80e9412b319e6ae48945e78a5b60174abee8327de3a9d137c9dba575ae