Coupler.io live artifact
A live artifact is a self-contained HTML page registered with Cowork that persists across sessions and re-fetches data from MCP connectors every time it opens. This skill covers the specific pitfalls of wiring one to a Coupler.io dataflow. Most generic widget guidance (CDN allowlist, sandboxing) lives in Cowork's create_artifact tool description — read that first. This skill captures the Coupler-specific traps that cost real time the first time someone hits them.
When to use
The user wants a re-openable view over Coupler.io data: KPIs, funnel, leaderboard, time series, comparison. They have an existing dataflow or are pointing at a data set in their Coupler workspace. The output is a .html file registered via mcp__cowork__create_artifact.
Process
Step 1 — Probe in chat first
Before writing a single line of HTML, run these chat-side calls and read the output. All Coupler operations go through the single dispatcher tool coupler: coupler({verb: "call", name: "<operation>", args: {...}}). Args are snake_case.
search-datasets({query: "<dataflow name>"}) to find the dataflow_id, last_dataset_snapshot_id, and the dataset id.
get-schema({dataset_snapshot_id}) and read ai_context carefully. It documents column meanings, the funnel order, the SQL conventions, and known caveats. Most pitfalls are flagged here.
get-data({dataset_snapshot_id, query: "SELECT * FROM data LIMIT 3"}) to see the actual row shape. Confirm what the schema claims.
get-data({dataset_snapshot_id, query: "SELECT DISTINCT col_X FROM data ORDER BY col_X"}) for any column you intend to expose as a filter dropdown — pre-compute the full enum.
- Verify the stated grain. The schema's
ai_context often claims one row per (entity × time), but real data sometimes has finer grain. Run:SELECT col_<id>, col_<time>, COUNT(*) cnt FROM data GROUP BY col_<id>, col_<time> HAVING cnt > 1 LIMIT 5
If this returns rows, the grain is finer than advertised — the leaderboard query must GROUP BY <id> and SUM(...) everything, otherwise duplicates show up.
Read references/coupler-conventions.md once before writing SQL. It encodes Coupler's column/value conventions you will hit immediately.
Step 2 — Decide the refresh strategy
Two modes, pick by how stale the user can tolerate:
| Mode |
Behavior |
Latency |
When to use |
| Live snapshot (default) |
Each artifact open calls list-datasets({dataflow_id}), takes last_dataset_snapshot_id, queries with get-data. |
Fast (~1 RTT). |
Daily checks, dashboards, any case where the user is fine with the last scheduled run. |
| Forced refresh (button) |
Manual button calls run-dataflow({dataflow_id}), polls list-datasets every 5 s for up to 5 min until snapshot ID changes, then re-queries. |
30 s – several min. |
"I want fresh data right now" buttons. Never on every page load — too slow and wasteful. |
Default to live-snapshot for load. Add a "Refresh data" button for forced refresh. Show last_success_run_at ("Last run: 5h ago") in the header so the user knows how stale things are.
Step 3 — Pick widget MCP tools
Only list MCP tools you actually call from the widget in the mcp_tools parameter of create_artifact. There is a single Coupler tool — the dispatcher — but its exact name is specific to this Cowork installation and must never be hardcoded. Find it yourself: look through your own available tools for the one matching mcp__<connector-id>__coupler, where <connector-id> is a UUID tied to this installation's registered Coupler MCP connector. Every installation has a different UUID — a skill that ships a literal one will silently call a tool name that doesn't exist anywhere else.
- Whatever you find, list it once in
mcp_tools; it covers all operations.
Widget-side calls take the form:
window.cowork.callMcpTool("<your discovered mcp__<connector-id>__coupler tool name>",
{ verb: "call", name: "list-datasets", args: { dataflow_id: DATAFLOW_ID } })
Operations the widget uses: list-datasets (snapshot lookup — accepts dataflow_id alone), get-data (runs SQL), run-dataflow (only if a refresh button exists).
Do not use:
search-datasets with only dataflow_id — its validator rejects this and returns "At least one of query, source, or name must be provided".
get-dataflow for snapshot lookup — it returns config (sources/destinations) only, no snapshot ID.
Step 4 — Write the artifact HTML
Start from references/widget-template.html — it's a minimal working scaffold with the right boilerplate (Chart.js CDN tag, snapshot lookup, localStorage filter persistence, light-mode styling).
Required helpers (in references/snippets.js, copy-paste into the artifact):
- Tolerant MCP response parser. Widget-side
callMcpTool wraps results inconsistently — sometimes direct JSON, sometimes {content:[{type:"text",text:"..."}]}, sometimes prose-prefixed text like "Total datasets: 1\n\n[...]". Use the snippet's parseToolResult + tryJsonFromText — they handle all variants.
- Timezone-safe month strings. Never use
new Date(y, m, 1).toISOString().slice(0, 10) — for users east of UTC it returns the previous day, silently producing zero-row queries. Use the snippet's monthFirst / prevMonthFirst.
- Quote stripping. Coupler stores string columns with embedded double quotes (e.g.
col_1 = '"Looker Studio"'). SQL WHERE matches must include the quotes; display layer must strip them. Use the snippet's stripQuotes for display, and quote values in SQL like col_1 = '"${value}"'.
- Run-and-poll. If the widget has a refresh button, copy the snippet's
refreshData flow.
- Chart.js canvas wrapping. With
responsive: true, maintainAspectRatio: false, every <canvas> must sit inside a parent with explicit height (e.g. <div style="position:relative;height:260px;width:100%"><canvas/></div>). Without it, Chart.js silently collapses the canvas and renders a broken-image icon.
- Allowed CDN libs only. Chart.js, Grid.js, Mermaid — exact tags from Cowork's
create_artifact description (with integrity and crossorigin). Anything else must be inlined.
localStorage for filter/sort state. Persist the user's dropdown selections and sort order between opens — that's what makes a daily-check view feel right.
Step 5 — Verify in the widget runtime
Probing in chat verifies data shape but not widget-runtime behavior. The two failure modes that are invisible to chat-side testing:
- MCP-wrapper shape differences — same tool returns one shape in chat, another shape (often prose-prefixed text) in widgets.
- Timezone bugs —
new Date(...).toISOString() behaves differently per user, and the symptom is silent zeros, not an error.
After registering the artifact, verify the rendered widget shows non-zero values for a known-populated month. If KPIs all read 0, suspect the timezone bug or the quote convention before suspecting the data.
Step 6 — Register the artifact
mcp__cowork__create_artifact({
id: "<kebab-slug>",
html_path: "<absolute path to the .html in outputs>",
description: "<one-line summary of what it shows>",
mcp_tools: ["<your discovered mcp__<connector-id>__coupler tool name, from Step 3>"]
})
For updates: mcp__cowork__update_artifact({id, html_path, update_summary}). Include a real update_summary — it's shown to the user in the approval prompt.
Rules & Edge Cases
- Always read
ai_context from get-schema before writing SQL. It documents column quote conventions, the funnel order, and known caveats. Skipping this step is the most common time-waster.
- Verify dataset grain with a
GROUP BY ... HAVING COUNT(*) > 1 probe. Don't trust the schema's stated grain. If the real grain is finer than advertised, your leaderboard will show duplicates.
- Never hardcode
dataset_snapshot_id. It rotates on every dataflow run. Always resolve via list-datasets({dataflow_id}) (through the dispatcher) at load time.
- Default to live-snapshot loads, gate
run-dataflow behind an explicit button. Triggering a fresh run on every artifact open turns a 1-second load into a 1-minute load. Users will hate it.
- Coupler's string-quoting convention is not stable. Some dataflows/snapshots store strings with embedded double quotes (
col_X = '"Looker Studio"'), others do not (col_X = 'Looker Studio'). The convention can flip between snapshots of the same dataflow. Always probe SELECT * FROM data LIMIT 1 and look at the actual values before writing SQL filters. Defense in depth: write filters that match both conventions, e.g. col_X IN ('value', '"value"'). Keep stripQuotes on the display layer as a no-op safety net.
- Build YYYY-MM-01 strings manually. Never via
toISOString(). Use ${y}-${String(m+1).padStart(2,"0")}-01. The timezone bug is silent — it produces zero-row queries, not errors.
- Chart.js canvases need a fixed-height parent. Wrap every
<canvas> in <div style="position:relative;height:Npx;width:100%">. Otherwise canvases collapse to 0 and you get a broken-image fallback.
- Only the CDN libs listed in the
create_artifact tool description load. Chart.js, Grid.js, Mermaid. Use the exact <script> tags including integrity and crossorigin. Other CDNs are blocked.
- The widget runs in light mode. Set
:root { color-scheme: light } and use a light background with dark text.
- All Coupler operations go through the
coupler dispatcher. mcp_tools lists only the dispatcher tool; widget calls pass {verb: "call", name, args} with snake_case args. The operation set can change — discover via {verb: "tools"} rather than trusting memory. The dispatcher's widget-runtime response wrapping has not been end-to-end verified; the tolerant parser in references/snippets.js is designed to absorb extra wrapping, but the first artifact built against the dispatcher must complete the Step 5 verification.
mcp_tools array must list every tool the widget actually calls (for Coupler, that is just the dispatcher). Tools not listed will fail at runtime.
- Filter dropdown values: pre-compute, don't query in the widget. Pulling distinct values is slow and pointless when you already know the enum at build time.
- End-to-end widget verification is not optional. Probe-in-chat catches data-shape bugs but misses widget-wrapper and timezone bugs. Always reload the rendered widget once and confirm non-zero values for a known-populated period.
table-layout: fixed collapses auto-width columns when other columns sum near the table width. If a column needs to be flexible but visible, give it an explicit width (e.g. 280px), not auto. Wrap the table in overflow-x:auto as a fallback.
Reference files
references/coupler-conventions.md — Coupler quote convention, grain caveats, ai_context tips, MCP tool quirks. Read before writing SQL.
references/snippets.js — Drop-in helpers: parseToolResult, tryJsonFromText, monthFirst, prevMonthFirst, stripQuotes, runQuery, fetchSnapshotInfo, refreshData. Copy what you need into the artifact.
references/widget-template.html — Minimal working scaffold (snapshot lookup, KPI cards, Chart.js, localStorage state, refresh button). Start here for new artifacts.
Self-improvement
When the user flags a new Coupler quirk or widget-runtime gotcha, append it to Rules & Edge Cases. When the user approves a finished artifact that demonstrates a useful pattern (e.g. a clean way to render a stacked-bar funnel with destination breakdown), copy it to references/examples/<descriptive-name>.html for future runs to anchor on.
1---2name: coupler-live-artifact3description: Build a live Cowork artifact (persistent HTML widget) backed by a Coupler.io dataflow. Use this skill whenever the user wants a live dashboard, persistent widget, daily-check page, or interactive explorer over Coupler.io data — including phrases like "live artifact for Coupler", "Coupler dashboard widget", "build a widget over a Coupler dataset", "Coupler.io live dashboard", "build a daily dashboard from a Coupler dataflow", or any time they ask to render Coupler data in a re-openable view that auto-refreshes. Triggers even when the user does not say "skill" or "artifact" but describes the same outcome (e.g. "I want a page I can check every morning that pulls my Coupler data").4---56# Coupler.io live artifact78A live artifact is a self-contained HTML page registered with Cowork that persists across sessions and re-fetches data from MCP connectors every time it opens. This skill covers the specific pitfalls of wiring one to a Coupler.io dataflow. Most generic widget guidance (CDN allowlist, sandboxing) lives in Cowork's `create_artifact` tool description — read that first. This skill captures the Coupler-specific traps that cost real time the first time someone hits them.910## When to use1112The user wants a re-openable view over Coupler.io data: KPIs, funnel, leaderboard, time series, comparison. They have an existing dataflow or are pointing at a data set in their Coupler workspace. The output is a `.html` file registered via `mcp__cowork__create_artifact`.1314## Process1516### Step 1 — Probe in chat first1718Before writing a single line of HTML, run these chat-side calls and read the output. All Coupler operations go through the single dispatcher tool `coupler`: `coupler({verb: "call", name: "<operation>", args: {...}})`. Args are snake_case.19201. `search-datasets({query: "<dataflow name>"})` to find the `dataflow_id`, `last_dataset_snapshot_id`, and the dataset `id`.212. `get-schema({dataset_snapshot_id})` and **read `ai_context` carefully**. It documents column meanings, the funnel order, the SQL conventions, and known caveats. Most pitfalls are flagged here.223. `get-data({dataset_snapshot_id, query: "SELECT * FROM data LIMIT 3"})` to see the actual row shape. Confirm what the schema claims.234. `get-data({dataset_snapshot_id, query: "SELECT DISTINCT col_X FROM data ORDER BY col_X"})` for any column you intend to expose as a filter dropdown — pre-compute the full enum.245. **Verify the stated grain.** The schema's `ai_context` often claims one row per `(entity × time)`, but real data sometimes has finer grain. Run:25 ```sql26 SELECT col_<id>, col_<time>, COUNT(*) cnt FROM data GROUP BY col_<id>, col_<time> HAVING cnt > 1 LIMIT 527 ```28 If this returns rows, the grain is finer than advertised — the leaderboard query must `GROUP BY <id>` and `SUM(...)` everything, otherwise duplicates show up.2930Read `references/coupler-conventions.md` once before writing SQL. It encodes Coupler's column/value conventions you will hit immediately.3132### Step 2 — Decide the refresh strategy3334Two modes, pick by how stale the user can tolerate:3536| Mode | Behavior | Latency | When to use |37|---|---|---|---|38| **Live snapshot** (default) | Each artifact open calls `list-datasets({dataflow_id})`, takes `last_dataset_snapshot_id`, queries with `get-data`. | Fast (~1 RTT). | Daily checks, dashboards, any case where the user is fine with the last scheduled run. |39| **Forced refresh** (button) | Manual button calls `run-dataflow({dataflow_id})`, polls `list-datasets` every 5 s for up to 5 min until snapshot ID changes, then re-queries. | 30 s – several min. | "I want fresh data right now" buttons. Never on every page load — too slow and wasteful. |4041Default to live-snapshot for load. Add a "Refresh data" button for forced refresh. Show `last_success_run_at` ("Last run: 5h ago") in the header so the user knows how stale things are.4243### Step 3 — Pick widget MCP tools4445Only list MCP tools you actually call from the widget in the `mcp_tools` parameter of `create_artifact`. There is a single Coupler tool — the dispatcher — but **its exact name is specific to this Cowork installation and must never be hardcoded.** Find it yourself: look through your own available tools for the one matching `mcp__<connector-id>__coupler`, where `<connector-id>` is a UUID tied to this installation's registered Coupler MCP connector. Every installation has a different UUID — a skill that ships a literal one will silently call a tool name that doesn't exist anywhere else.4647- Whatever you find, list it once in `mcp_tools`; it covers all operations.4849Widget-side calls take the form:5051```js52window.cowork.callMcpTool("<your discovered mcp__<connector-id>__coupler tool name>",53 { verb: "call", name: "list-datasets", args: { dataflow_id: DATAFLOW_ID } })54```5556Operations the widget uses: `list-datasets` (snapshot lookup — accepts `dataflow_id` alone), `get-data` (runs SQL), `run-dataflow` (only if a refresh button exists).5758Do **not** use:59- `search-datasets` with only `dataflow_id` — its validator rejects this and returns "At least one of query, source, or name must be provided".60- `get-dataflow` for snapshot lookup — it returns config (sources/destinations) only, no snapshot ID.6162### Step 4 — Write the artifact HTML6364Start from `references/widget-template.html` — it's a minimal working scaffold with the right boilerplate (Chart.js CDN tag, snapshot lookup, `localStorage` filter persistence, light-mode styling).6566Required helpers (in `references/snippets.js`, copy-paste into the artifact):67681. **Tolerant MCP response parser.** Widget-side `callMcpTool` wraps results inconsistently — sometimes direct JSON, sometimes `{content:[{type:"text",text:"..."}]}`, sometimes prose-prefixed text like `"Total datasets: 1\n\n[...]"`. Use the snippet's `parseToolResult` + `tryJsonFromText` — they handle all variants.692. **Timezone-safe month strings.** Never use `new Date(y, m, 1).toISOString().slice(0, 10)` — for users east of UTC it returns the previous day, silently producing zero-row queries. Use the snippet's `monthFirst` / `prevMonthFirst`.703. **Quote stripping.** Coupler stores string columns with embedded double quotes (e.g. `col_1 = '"Looker Studio"'`). SQL `WHERE` matches must include the quotes; display layer must strip them. Use the snippet's `stripQuotes` for display, and quote values in SQL like `col_1 = '"${value}"'`.714. **Run-and-poll.** If the widget has a refresh button, copy the snippet's `refreshData` flow.725. **Chart.js canvas wrapping.** With `responsive: true, maintainAspectRatio: false`, every `<canvas>` must sit inside a parent with explicit height (e.g. `<div style="position:relative;height:260px;width:100%"><canvas/></div>`). Without it, Chart.js silently collapses the canvas and renders a broken-image icon.736. **Allowed CDN libs only.** Chart.js, Grid.js, Mermaid — exact tags from Cowork's `create_artifact` description (with `integrity` and `crossorigin`). Anything else must be inlined.747. **`localStorage` for filter/sort state.** Persist the user's dropdown selections and sort order between opens — that's what makes a daily-check view feel right.7576### Step 5 — Verify in the widget runtime7778Probing in chat verifies data shape but **not** widget-runtime behavior. The two failure modes that are invisible to chat-side testing:7980- **MCP-wrapper shape differences** — same tool returns one shape in chat, another shape (often prose-prefixed text) in widgets.81- **Timezone bugs** — `new Date(...).toISOString()` behaves differently per user, and the symptom is silent zeros, not an error.8283After registering the artifact, verify the rendered widget shows non-zero values for a known-populated month. If KPIs all read 0, suspect the timezone bug or the quote convention before suspecting the data.8485### Step 6 — Register the artifact8687```88mcp__cowork__create_artifact({89 id: "<kebab-slug>",90 html_path: "<absolute path to the .html in outputs>",91 description: "<one-line summary of what it shows>",92 mcp_tools: ["<your discovered mcp__<connector-id>__coupler tool name, from Step 3>"]93})94```9596For updates: `mcp__cowork__update_artifact({id, html_path, update_summary})`. Include a real `update_summary` — it's shown to the user in the approval prompt.9798## Rules & Edge Cases99100- **Always read `ai_context` from `get-schema` before writing SQL.** It documents column quote conventions, the funnel order, and known caveats. Skipping this step is the most common time-waster.101- **Verify dataset grain with a `GROUP BY ... HAVING COUNT(*) > 1` probe.** Don't trust the schema's stated grain. If the real grain is finer than advertised, your leaderboard will show duplicates.102- **Never hardcode `dataset_snapshot_id`.** It rotates on every dataflow run. Always resolve via `list-datasets({dataflow_id})` (through the dispatcher) at load time.103- **Default to live-snapshot loads, gate `run-dataflow` behind an explicit button.** Triggering a fresh run on every artifact open turns a 1-second load into a 1-minute load. Users will hate it.104- **Coupler's string-quoting convention is not stable.** Some dataflows/snapshots store strings with embedded double quotes (`col_X = '"Looker Studio"'`), others do not (`col_X = 'Looker Studio'`). The convention can flip between snapshots of the same dataflow. Always probe `SELECT * FROM data LIMIT 1` and look at the actual values before writing SQL filters. Defense in depth: write filters that match both conventions, e.g. `col_X IN ('value', '"value"')`. Keep `stripQuotes` on the display layer as a no-op safety net.105- **Build YYYY-MM-01 strings manually.** Never via `toISOString()`. Use `${y}-${String(m+1).padStart(2,"0")}-01`. The timezone bug is silent — it produces zero-row queries, not errors.106- **Chart.js canvases need a fixed-height parent.** Wrap every `<canvas>` in `<div style="position:relative;height:Npx;width:100%">`. Otherwise canvases collapse to 0 and you get a broken-image fallback.107- **Only the CDN libs listed in the `create_artifact` tool description load.** Chart.js, Grid.js, Mermaid. Use the exact `<script>` tags including `integrity` and `crossorigin`. Other CDNs are blocked.108- **The widget runs in light mode.** Set `:root { color-scheme: light }` and use a light background with dark text.109- **All Coupler operations go through the `coupler` dispatcher.** `mcp_tools` lists only the dispatcher tool; widget calls pass `{verb: "call", name, args}` with snake_case args. The operation set can change — discover via `{verb: "tools"}` rather than trusting memory. The dispatcher's widget-runtime response wrapping has not been end-to-end verified; the tolerant parser in `references/snippets.js` is designed to absorb extra wrapping, but the first artifact built against the dispatcher must complete the Step 5 verification.110- **`mcp_tools` array must list every tool the widget actually calls** (for Coupler, that is just the dispatcher). Tools not listed will fail at runtime.111- **Filter dropdown values: pre-compute, don't query in the widget.** Pulling distinct values is slow and pointless when you already know the enum at build time.112- **End-to-end widget verification is not optional.** Probe-in-chat catches data-shape bugs but misses widget-wrapper and timezone bugs. Always reload the rendered widget once and confirm non-zero values for a known-populated period.113- **`table-layout: fixed` collapses `auto`-width columns when other columns sum near the table width.** If a column needs to be flexible but visible, give it an explicit width (e.g. `280px`), not `auto`. Wrap the table in `overflow-x:auto` as a fallback.114115## Reference files116117- `references/coupler-conventions.md` — Coupler quote convention, grain caveats, ai_context tips, MCP tool quirks. Read before writing SQL.118- `references/snippets.js` — Drop-in helpers: `parseToolResult`, `tryJsonFromText`, `monthFirst`, `prevMonthFirst`, `stripQuotes`, `runQuery`, `fetchSnapshotInfo`, `refreshData`. Copy what you need into the artifact.119- `references/widget-template.html` — Minimal working scaffold (snapshot lookup, KPI cards, Chart.js, localStorage state, refresh button). Start here for new artifacts.120121## Self-improvement122123When the user flags a new Coupler quirk or widget-runtime gotcha, append it to `Rules & Edge Cases`. When the user approves a finished artifact that demonstrates a useful pattern (e.g. a clean way to render a stacked-bar funnel with destination breakdown), copy it to `references/examples/<descriptive-name>.html` for future runs to anchor on.