本文へ移動
cccskills
無料GitHub で公開日本語紹介

dashboard-management

Analyticsのダッシュボードやパネルを作成・配置・整理し、共有やグラフへの日付・データ注記の追加まで行うためのスキルです。

原文原文の説明を見る

Create, lay out, organize, or share Analytics dashboards and panels: sources, placement, folders, verification, chart markers. Use to create a dashboard, to move, reorder, or lay out panels, or to add a key date or data note; a small edit of one existing panel needs no skill.

BuilderIO/agent-nativeスター 7,525(GitHub 7,525 + cccskills 0)

実行スコア 未検証

インストール方法を見る

こんなときに便利

  • 分析ダッシュボードを新しく作りたいとき
  • パネルの順序や大きさをまとめて変えたいとき
  • ローンチ日をグラフに表示したいとき
  • データ異常の期間を注記したいとき

日本語での紹介

できること

Analyticsのダッシュボードを作成し、データソースとクエリを指定したパネルを配置できます。パネルの順序や幅・高さを調整し、ダッシュボードをフォルダーで整理したり、共有したりする作業も対象です。保存時には設定やクエリの検証を行い、変更履歴から以前の状態を確認・復元できます。時系列グラフには、リリースやローンチなどの重要日を示すマーカーを追加できます。計測障害などで数値を信頼できない期間は、データ注記として期間や影響対象を記録できます。新規作成では、用意されたカタログやスキーマ情報を参照し、指標の意味が数値を左右するほど曖昧な場合は確認してから構成します。

こんなときに便利

新しい分析ダッシュボードを作りたいときや、複数パネルのレイアウトをまとめて変更したいときに向いています。ローンチ日をグラフに示したり、データ異常の期間を共有したりする場面にも役立ちます。単一パネルの小さな編集には、このスキルは不要とされています。

使い方の例

  • 「週次の指標をまとめたダッシュボードを作成してください」
  • 「パネルの順序を変更して、グラフの幅を調整してください」
  • 「ローンチ日と計測不具合の期間をグラフに注記してください」

注意点

資料は途中までのため、後半に記載された内容は反映していません。ダッシュボードの保存・共有にはAnalytics環境の権限が必要です。BigQueryをデータソースにする場合、クエリの検証と課金上限に関する設定が記載されています。

この紹介文は、公開されている SKILL.md をもとに AI(OpenAI Codex)が作成しました。正確な仕様は下の原文を確認してください。

含まれるファイル(1)

  • SKILL.md63.0 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

Dashboard Management

Dashboards are the canonical SQL-backed Analytics resources. New dashboards, saved analyses, reports, and bespoke extensions are represented as dashboard artifacts—not as separate user-facing Analytics objects. Legacy analysis tables and actions remain available only for compatibility.

Storage

Current storage:

TablePurpose
dashboardsExplorer and SQL dashboard records
dashboard_foldersPersonal and shared SQL dashboard folders
dashboard_viewsSaved filter presets per dashboard
dashboard_sharesStandard framework share grants
dashboard_folder_sharesStandard folder share grants
dashboard_revisionsBounded dashboard history snapshots
analysesSaved ad-hoc analysis records
analysis_revisionsBounded analysis history snapshots
analysis_sharesStandard framework share grants for analyses

Legacy settings keys such as u:<email>:dashboard-*, u:<email>:sql-dashboard-*, o:<orgId>:sql-dashboard-*, and adhoc-analysis-* are still read as a fallback and copied into SQL on access. Do not create new dashboard settings rows.

Dashboard folders

Dashboard folders are SQL-backed, access-scoped containers for organizing SQL dashboards from /dashboards:

  • personal folders are private to their owner.
  • shared folders are organization-visible and require an active organization.
  • Use list-dashboard-folders, create-dashboard-folder, and set-dashboard-folder for folder reads and membership changes.
  • A folder never expands dashboard access. A personal folder can contain only an owned private dashboard; a shared folder can contain only an org-visible dashboard. Use the normal dashboard sharing actions separately.

For organization-wide consolidation, use migrate-analytics-artifacts first with dryRun: true. The write requires an organization owner/admin and the exact confirmation token MIGRATE_ANALYTICS_ARTIFACTS. It materializes organization-scoped legacy settings, creates dashboard blocks for saved analyses and standalone extensions, archives exact duplicates, copies shares, and removes legacy settings keys only after SQL rows are materialized. Source rows remain recoverable. Private member-only rows are intentionally outside this organization-scoped operation.

Use mutate-dashboard for existing dashboard edits. It resolves the current user/org context, validates the resulting config, writes the SQL-backed record, syncs collab, and returns a compact result with a verified flag. Use update-dashboard for new full-config saves, UI full-config saves, or explicitly requested low-level JSON-pointer edits.

Every meaningful dashboard save snapshots the previous state into dashboard_revisions. Use list-dashboard-revisions to inspect available undo points and restore-dashboard-revision to restore one; restore snapshots the current state first, then syncs open dashboard editors.

Each revision's changes names what the save that left it behind changed (panel-added, column-resized, filter-changed, version-restored, and so on), so restoring it undoes that change and every later one. To answer "undo the column I widened", pick the revision whose changes match instead of guessing from times. changesStatus is absent for revisions saved before changes were recorded and unreadable when the stored list is corrupt; say you can't tell what those changed rather than treating them as empty.

On the page, an editor's own saves show a toast that names the change its Undo takes back, says "Saved for everyone" (org or public visibility; "Saved" when private), and counts the others who have the dashboard open. That Undo restores the revision its save left (a full-config update-dashboard save returns it as revisionId), but only while the dashboard is as that save left it: once someone else has saved, it fails rather than taking their change back. For "undo my last change" in chat, restore the newest revision from list-dashboard-revisions. The History panel shows each revision's first change with "+N more".

Saved analyses follow the same undo model with analysis_revisions, list-analysis-revisions, and restore-analysis-revision.

Dashboards open in View mode. An editor switches the header toggle to Edit before drags, panel resizing, renames, column edits, and adding, editing, showing or hiding key dates and data notes on the page save for everyone; the chat context reports the current mode. A table column width dragged in View mode is stored only in that person's browser, so "widen this column for me" is a drag in View mode, not a dashboard change. config.columns[].width is the shared width everyone sees; set it only when asked to change the dashboard itself.

Never use db-patch, raw SQL, or settings-key edits to create or modify a dashboard config. Those bypass the dashboard action's access checks, SQL validation, collab sync, and verification. If a dashboard action fails because the argument shape was wrong, fix that action's arguments and retry once — do not switch to db-patch or raw SQL.

GitHub folder sync

A linked folder mirrors its SQL dashboards to <path>/<dashboardId>.json in a GitHub repo. Read docs/dashboard-github-folder-sync.md before changing or explaining it.

  • Run preview-dashboard-folder-github-sync before any sync. It writes nothing.
  • apply-dashboard-folder-github-sync pulls from GitHub. It writes only the panels, order, and settings that changed on the GitHub side.
  • export-dashboard-folder-to-github opens one PR with the app's changes. It refuses while an earlier export PR is still open.
  • configure-dashboard-folder-github-sync links or unlinks a folder. It stores no credentials.
  • A unit changed on both sides is a conflict. It is reported and not applied. Do not resolve it by overwriting either side without asking the user.
  • Explorer dashboards, sharing, and deletions never sync. A synced dashboard cannot be permanently deleted; archive it or unlink its folder first.

Valid Panel Sources

panel.source is a backend selector, not a table name. It must be one of:

SourceQuery shape
bigqueryLiteral warehouse SQL. Table names belong inside the SQL string.
ga4JSON descriptor for the Google Analytics Data API.
amplitudeJSON descriptor for an Amplitude query.
first-partyRead-only SQL over this template's analytics_events table, usually via query-agent-native-analytics.

Do not use app-db as a dashboard source. For first-party events collected through /track, use source: "first-party" or the query-agent-native-analytics action rather than raw internal db-query.

Per-article content metrics also read through first-party: analytics_content_items (one row per article, with author and publish date), analytics_content_metric_snapshots (one value per item, day and metric), and analytics_content_collection_runs (one row per daily collection step). A daily job fills them, so a panel over them never calls BigQuery or a provider. A missing snapshot means "not collected", never zero: show a step's latest run status (failed, cost_limited, not_configured) instead of rendering a gap as 0. These tables cannot be joined with analytics_events in one query. Turn collection on with configure-content-collection, check it with get-content-collection, and rerun a step with run-content-collection.

Metrics are ga4_pageviews (per blog article) and search_console_clicks / search_console_impressions (per article, plus non-article pages stored as search_console items). Search Console data is two to three days late, so the newest days have no row yet; read them as still arriving. Search Console collection stays off until a property owner adds the service account that get-content-collection returns as serviceAccount.email; then set its searchConsoleSites and turn it on.

The signups step writes signups_first_touch per article: people who first landed on the article and later signed up, on the day of that first visit, so a window's days add up to its total. It is the only step that reads the warehouse. It stays not_configured until configure-content-collection sets its signups tables, signupSource, signupsSince and maxBytesBilled. Measure the cap with estimate-content-signups-cost (a dry run, free) and get the spend approved before turning the step on. Each run dry-runs first, stops as cost_limited without querying when the estimate is over the cap, and runs the join with that cap as BigQuery's billing limit.

The catalog's "Content" dashboard (content-view) is the reference for these tables: start from its panels before writing new content SQL. Its Articles panel shows why a metric is missing (failed, not set up, still arriving) in a column next to the blank number; keep that pattern. The first-party SQL policy rejects functions outside its allow-list (for example concat_ws) and reads CASE ( as a function call, so join a status column instead of a scalar subquery inside CASE. Its trend chart uses wide rows (yKeys), not pivot: a pivot fills a missing series with 0, so a day a source didn't report would draw as a zero instead of a gap. Content is a built-in: turning collection on with configure-content-collection lists it in the organization's Dashboards at content-view (contentDashboardId in the result). It is read-only; build on it with duplicate-dashboard.

There is no dbt Semantic Layer panel source. For a Semantic Layer metric, answer in chat with query-dbt-semantic-metric; build a warehouse panel only on a table the owner names.

AI-generated first-party panels are dashboard-time-bound by default. Set config.timeScope to "dashboard" and include the matching dashboard time filter in the SQL. The allowed values are:

  • dashboard: use the dashboard-selected time range; the default for ordinary metrics.
  • fixed-window: use an explicit bounded window independent of the dashboard filter.
  • cohort-history: use the bounded history of an explicitly defined cohort.
  • all-time: scan all available history; use only when the user requests it and put all-time, lifetime, or historical in the title or description.

{{timeRange}} requires an explicit matching filters entry with id: "timeRange" and type: "select". {{<id>Start}} and {{<id>End}} require a matching filters entry with that id and type: "date-range". Do not rely on undeclared time variables. Server validation rejects unbound first-party SQL, so declare the filter or choose an explicit non-dashboard scope before saving.

A bound anywhere in the SQL is not the same as every CTE having its own bound. If a panel has multiple top-level CTEs (WITH a AS (...), b AS (...)) and more than one of them reads analytics_events, EVERY one of those CTEs needs its own {{timeRange}}/{{<id>Start}}/{{<id>End}} reference or literal date bound — not just the final SELECT or one sibling CTE. A CTE that computes something like "this user's first-ever active day" by scanning analytics_events with no bound at all will full-table-scan on every render even though the panel looks time-bound overall (root cause of a 2026-07-25 production incident: several dashboards had exactly this shape). Server validation checks each top-level CTE independently now, so this fails at save time — but write it right the first time: bound every CTE, or use config.timeScope: "cohort-history" only for a CTE that is genuinely defining a cohort (e.g. a first-seen date), never as a way to skip bounding an ordinary activity scan.

Creating A Dashboard

When the user asks for a dashboard:

  1. Start from the preloaded <resource scope="analytics-catalog"> references (catalog-first), else one find-data. If a relevant entry exists, use its table, columns, queryTemplate, and gotchas verbatim.
  2. If a metric definition, date range, or grain is ambiguous and the choice would change the panel's numbers, use the ask-question clarifying tool once before building. Skip it when the dictionary or the user already settled it.
  3. If a metric is not documented, do not guess column names and do not ask the user for them. Find the table and columns with search-bigquery-schema (or the provider's own schema action), then propose a dictionary entry with save-data-dictionary-entry.
  4. Build a complete SqlDashboardConfig with name and panels. Optionally set top-level columns (1–6, default 2) to control how many grid columns the panels before any section use.
  5. Every panel needs id, title, source, chartType, width, and sql. width is the number of grid columns the panel spans (1..6, clamped to the active section's column count). height is optional; when set, it is 1–6 units of 180px, and when omitted the panel sizes to its content. Section panels skip source and sql and may set their own columns (1–6) to override the dashboard default for the panels following the section. Extension panels (chartType: "extension") also skip source and sql; use config.extensionId for ordinary author-selected shared embeds. Use config.extensionSlotId only when the user explicitly asks for a personal/per-viewer slot (see "Embedding An Extension As A Panel").
  6. Persist with update-dashboard (load it with tool-search — it is not on the initial tool surface), not raw SQL or settings writes. For a large first-party dashboard, load compose-dashboard the same way in one tool-search call.
  7. Navigate to it with pnpm action navigate --view=adhoc --dashboardId=<id>.

An explicit dashboard request authorizes the complete non-destructive build in the same turn. After querying or scaffolding, continue through extension-data seeding/refresh, dashboard save/embed, and navigation; do not ask whether to proceed or leave an empty Custom Block shell. Ask only when metric scope/grain is materially ambiguous, the change is destructive, or it has an external side effect such as sending email or outreach.

Layout is always 1 column when the available content width is below the md threshold (panels stack), then expands to the configured column count at/above it. The grid uses a container query, so it also stacks when the agent sidebar narrows the content pane — not only at narrow viewports. So picking 3 or 4 columns is fine — the renderer keeps narrow layouts readable automatically.

In Edit mode, drag a panel's right edge to change its grid width and its bottom edge to set a fixed height. Width is saved as grid columns; height is saved in 180px units. Existing panels without height keep content-based height until resized.

pnpm action update-dashboard --dashboardId weekly-metrics --config '<full json>'
pnpm action navigate --view=adhoc --dashboardId=weekly-metrics

The save path dry-runs BigQuery panels before persisting. If validation returns a provider error, fix the query and retry. Never work around validation by writing directly to a table.

Replicating Or Adapting Another Dashboard

When the user asks to replicate, clone, or adapt an existing dashboard into a new one, call search-dashboard-references with focused terms first. It searches accessible active saved dashboards (ids, names, descriptions, serialized config), including legacy ones. Inspect each result with get-sql-dashboard when kind is sql, or get-explorer-dashboard when it is explorer. A result is a reference, not proof its source is authoritative for the new request: check the provider and scope the user asked for before copying its source semantics, and do not route a replication request to first-party Analytics by default. This applies to a new dashboard, not to editing the dashboard that is already open.

Dual-Axis Charts

line, area, and bar panels can plot series against two y-axes. Reach for this whenever series share an x-axis but not a unit — a count next to a rate, or revenue next to a conversion percent. On a single axis the smaller series flattens into the baseline and reads as "no data."

{
  "id": "signups-vs-conversion",
  "title": "Signups vs conversion rate",
  "source": "first-party",
  "chartType": "line",
  "width": 1,
  "sql": "SELECT day, signups, conversion_rate FROM ...",
  "config": {
    "timeScope": "dashboard",
    "xKey": "day",
    "yKeys": ["signups", "conversion_rate"],
    "yFormatter": "number",
    "rightYKeys": ["conversion_rate"],
    "rightYFormatter": "percent"
  }
}
  • rightYKeys names series from yKeys; everything unnamed stays on the left.
  • rightYFormatter defaults to yFormatter when omitted.
  • Each axis is labelled with its series names (up to two per side), and tooltip values use the formatter of the axis the series belongs to.
  • At least one series must remain on the left. If rightYKeys names every series, or names a column the query never returned, the panel falls back to a single axis and shows a config warning rather than dropping the series.
  • Scheduled email reports render the same two scales, so a dual-axis panel is safe to put on a subscribed dashboard.

Key-Date Event Markers

Key dates mark a launch, release, or product change on line, area, bar, and combo charts with a date x-axis. Do not add a fake series or an extension for this. They live in two places:

  • Key-date sets hold moments that matter across charts, such as Launches or Product changes. A set belongs to the organization (to the user when there is no organization) and draws on every time chart of every dashboard. Read sets with list-key-dates, passing dashboardId for the ones a dashboard draws; add or edit a date with save-key-date (name the set with setName and it is created if missing); rename or create a set with save-key-date-set; remove with delete-key-date or delete-key-date-set. Prefer a set to copying one date into many panels.
  • Panel events in config.events hold a date that matters to one chart only, in the same shape:
"config": {
  "events": [
    { "date": "2026-10-14", "time": "00:01", "timeZone": "America/Los_Angeles", "label": "Product Hunt live" },
    { "date": "2026-10-14", "time": "12:00", "timeZone": "America/New_York", "label": "Launch posts on X, LinkedIn" }
  ]
}

A dashboard leaves sets off its charts with config.annotations, set through mutate-dashboard's setDashboard({ annotations: { hiddenKeyDateSets: [setId, ...] } }). The list replaces the sets already hidden, so include those that stay hidden; the hidden data notes are kept. In Edit mode, people do the same from the dashboard header's Annotations menu, which also adds and edits key dates and sets.

  • Each event is { date: "YYYY-MM-DD", label, time?: "HH:MM", timeZone?, color? }; color accepts SVG-safe hex values: #RGB, #RGBA, #RRGGBB, or #RRGGBBAA. Numeric x-axis timestamps use Unix epoch milliseconds. multiple are allowed. date is the calendar date in the event's own zone. Give time when the moment matters (a launch hour); timeZone is an IANA zone and needs a time. Without a zone, the time is UTC.
  • A badge sits on the x-axis at the date with a faint dashed guide line. Hovering the badge lists every event on that day with its time, shown in the event's own zone ("12:01 AM PT"). It adds no legend entry and does not change the series tooltip.
  • Events on the same day share one badge; on an hourly axis each timed event sits in its own hour, and an event without a time sits on the first hour shown for its date.
  • On a daily or coarser axis, a date missing from the data snaps to the nearest x value. On an hourly axis, an event without a time whose date has no hours shown is hidden. A date outside the visible range is hidden; a non-date x-axis shows nothing.
  • A set's date that a panel also lists in config.events draws once.
  • mutate-dashboard rejects malformed events and other chart types.
  • Scheduled email reports draw key dates and panel events: the guide line, badge, and label text, including on combo charts. Labels sit in rows above the plot: same-day labels join, a long one wraps at words, and days that find no room are counted, not cut.
  • A dashboard and its report emails draw the key dates of its owner's organization, minus the sets it hides, even for a viewer or reviewer in another organization. There list-key-dates returns editable: false, without member emails or, unless you can edit the dashboard, the hidden sets, and no sets at all when no chart there draws key dates: a date you save lands in your own organization's sets, not on that dashboard. For a Human Review preview, pass list-key-dates and list-data-notes the dashboardId and the same reviewPreview and reviewOrgId as get-sql-dashboard.

Data Notes

A data note says the numbers on a chart are wrong or unusual for a stretch of time: an incident (kind: "incident") or a tracking problem (kind: "data"). Use one instead of a key date whenever the point is "don't trust this range". Notes belong to the organization, like key-date sets, and a dashboard draws its owner organization's. Read them with list-data-notes, passing dashboardId for the ones a dashboard draws; add or change one with save-data-note (pass id to change only the fields given); remove with delete-data-note.

  • matchEvents lists the event names the problem affects. A note shows on every chart whose SQL filters on one of them: either side of = (in WHERE or CASE WHEN), an IN (...) item, or a simple CASE event WHEN value, matched exactly and case-sensitively; or a LIKE pattern (ILIKE in any case). A name the SQL only selects as a label, returns from THEN/ELSE, excludes (!=, <>, or a NOT over its comparison or parentheses), or writes in a comment does not count, and neither does = ANY(ARRAY[...]) or IN UNNEST([...]). unlessEvents skips charts that also filter on the corrected event. Pin a note to specific charts with panels: [{ dashboardId, panelId }] when no event filter identifies them.
  • startDate and endDate are YYYY-MM-DD. Leave out endDate while the problem is ongoing, and startDate when it has been wrong since before tracking began. Marking a note status: "fixed" needs an endDate; say what fixed it in fix. Pass null to clear a date or the fix.
  • Time charts shade the note's range, with an edge only where it starts or ends inside the chart, and a hoverable title. Rows sorted by something other than date shade each run of the note's rows, never a row between them, under one title. Incidents are red; open data problems amber; fixed data problems grey. Other charts, and a time chart whose x values aren't dates, flag the note by the title and write it under the chart when its dates overlap the chart's time range: the panel's own range if it overrides the dashboard's. A chart whose SQL has no time range, or one the app can't read such as a quarter its SQL works out, does so while the note is ongoing or ended in the last 30 days.
  • A dashboard hides a note on one chart with setDashboard({ annotations: { hiddenDataNotes: [{ noteId, panelId }, ...] } }), listing the notes already hidden that stay hidden; hiddenKeyDateSets is kept.
  • In Edit mode, people add a note from a chart's menu ("Add data note", pinned to that chart) or the Annotations menu; a note's hover card offers Edit, Mark fixed, and Hide on this chart, and the chart's menu offers Hide for each of its notes. On a dashboard from another organization they can hide its notes and sets but not add or change them.
  • Scheduled email reports shade the same ranges, title each note once in the label rows above the plot alongside key dates (a title with no room left is counted), and write each note's text under the chart.

Adding A Marker

When someone asks to mark something on a chart, pick the kind from what a reader should take away:

  • A moment that explains a change (a launch, a release, a pricing change) is a key date. Call list-key-dates first and add it to the set it belongs in rather than starting a near-duplicate set. Give time and timeZone when the hour matters, as it does for launches. Use a panel's config.events only when the date matters to that one chart.
  • A stretch where the numbers are wrong or unusual (an outage, an undercount, a tracking gap) is a data note. Take matchEvents from the event names the charts' SQL filters on (get-sql-dashboard with panelIds), not from how the person phrased it, and pin panels when no event name identifies the charts.

Pass the open dashboard's id as dashboardId to save-key-date or save-data-note, and read the result's dashboard before replying. A note lists the charts it is on in shownOn; a key date says only whether the dashboard shows its set, in setShown. Neither promises a mark: a time chart draws one only over the dates it plots. When hint says it shows on none, fix the event names, or tell the person why. A hint with neither shownOn nor setShown means the dashboard can't draw it at all (an Explorer dashboard, or another organization's); it is still saved. A set or note the dashboard hides is that dashboard's choice, so ask before showing it.

To explain a spike or dip, read keyDateSets and dataNotes from get-sql-dashboard or view-screen first and cite what they say. If annotationsError is there instead, say the markers could not be read rather than that there are none. omittedKeyDateSetCount or omittedDataNoteCount means the list was cut to fit, so read list-key-dates or list-data-notes with dashboardId before saying a marker isn't there. keyDatesHint means no chart on the dashboard draws key dates, so don't cite one there. A note's panelIds are the charts it is on, but a time chart draws it only over the dates it plots (and, if its x values aren't dates, only when they overlap its time range), so cite it for a change only when the change falls within the note's dates. editable: false means they are the owner organization's, so cite them but don't change them.

Reusable Native Dashboard Patterns

The recent extension-backed dashboards in Builder Analytics cluster into a few repeatable compositions. Prefer these native panels, with a real SQL or Data Program result behind each one, when creating a replacement or a new dashboard:

PatternNative composition
Customer ROI / value realizationmetric KPI cards, line or area trends, table detail, and callout or section panels for the business narrative
Account engagement / outreachmetric coverage and adoption cards, a daily line trend, heatmap or table segmentation, and callout alerts
GTM pipeline / cross-sellfunnel for ordered stages, metric totals, bar or line trends, and a table for account-level follow-up
Win/loss analysissection groups with metric, table, bar, callout, and trend panels; use a Data Program for provider joins and evidence rows

Panel config.description values and a callout row's message support inline Markdown links. Use [label](https://example.com); raw HTML is not rendered.

Funnel panels use config.xKey for the stage label and config.yKey for a non-negative count or value. The renderer preserves the SQL row order, shows each stage's share of the first stage, and shows the change from the previous stage. Keep the intended stage order in SQL with ORDER BY.

When people can skip a stage or go around it (onboarding, setup flows), add config.funnelOutcomes so a skip never reads as drop-off. It names result columns of per-stage people counts, not shares: completedKey and leftKey are required, skippedKey is optional, and optionalKey names a boolean column that tags a stage Optional. yKey stays the people shown the stage, and completed + skipped + left must not exceed it; the rest are "moved on without either". Each bar then splits into those outcomes and the stage leads with the share who left the app there. A missing column draws the usual warning; a stage whose counts are not numbers or add up to more than shown draws a plain bar, and the panel check reports it under funnelOutcomes.

To let people click a table row and see its numbers split by app (or another column) in a side panel, add config.breakdowns: [{ column: "app", label: "App" }] and put the column in the panel SQL inside {{?breakdown}}…{{/breakdown}} blocks in both the select list and the GROUP BY, e.g. SELECT role{{?breakdown}}, {{breakdown}}{{/breakdown}}, COUNT(*) AS people … GROUP BY role{{?breakdown}}, {{breakdown}}{{/breakdown}}. The column must be a real column in scope at that point of the SQL; when a CTE aggregates first, carry it through that CTE. A normal load drops the blocks, so the panel costs nothing more until someone opens a row; one capped breakdown query then serves every row on the same filters. If the panel's rows already carry the column, no block is needed and no query runs. For web, desktop or mobile, first-party events carry properties.client_platform (web, electron, mobile); label electron Desktop. Rows match their breakdown on every column that is not a number, so give a numeric column that names rows (a week, a year, an ID) format: "text" or "date" in config.columns; unformatted numbers count as measures. The first format: "number" column is the group size that sorts, mutes and folds groups, so make it a count such as people, not an average or a rate. Breakdowns don't work on a pivoted table (config.pivot). The open breakdown is in view-screen as rowBreakdown.

When a dashboard is being migrated from an extension, create a new v2 copy, bind its panels to the real provider schema or Data Programs, and compare it with the original before retiring the extension-backed version. Do not invent customer-specific SQL, provider joins, cached rows, or extension ids in a catalog template. Existing dashboards remain readable while the native replacement is validated. Bespoke interaction flows, arbitrary layouts, and visualizations outside these contracts may remain Custom Blocks.

Sigma workbook import

The dashboard menu opens the Sigma import flow: a searchable table of every workbook the user can see in Sigma, with the owner of each migrated one and a Migrate button on the rest. Imports remain disabled for elements whose source project Analytics cannot verify: Sigma's connection API reports provider type and metadata but does not expose a BigQuery project identifier. Do not call migrate-sigma-workbook for an unverified source or treat an Analytics BigQuery credential as proof that it targets Sigma's project. Users can list workbooks with list-sigma-workbooks and inspect one with inspect-sigma-workbook. The feature is controlled by analytics.sigma-dashboard-migration and requires a Sigma connection granted to Analytics.

Use an existing Dispatch Sigma connection when it is granted to Analytics; the import flow discovers accessible connections without copying their Vault values. Any workspace member can connect a personal Sigma account with OAuth from the importer and choose the account's documented API region. OAuth tokens and the connection stay scoped to that user. If direct setup is needed, an organization owner or admin can still enter Sigma client credentials in the Analytics dialog. Analytics saves those values under Analytics-specific encrypted organization secrets and links them to an Analytics-only connection. Credential values must never pass through agent chat, action arguments, logs, source, or screenshots.

Each Sigma workbook migrates to at most one Analytics dashboard per organization. list-sigma-workbooks reads migration status only for workbooks the caller's Sigma connection returns, so status follows Sigma access. For a migrated workbook it returns the dashboard id, link, and owner email and display name to every org member, and the title and canOpen: true only when the caller can open the dashboard. Point users at that dashboard, or its owner for access, instead of importing again. Anyone whose Sigma login can open a workbook may retry its failed import or take over an import stuck for ten minutes; only the person who migrated a workbook may re-import it.

If Sigma adds verifiable source mapping in the future, the planned import will create a private snapshot in the user's personal Sigma folder. Other source types and unsupported elements will become review callouts. Preserve migration warnings and never describe a future result as parity-complete or automatically synchronized. Sigma's workbook-code API is public Beta and can omit unsupported workbook features before inspection receives the response.

The source tree ships four provider-free v2 manifests in server/lib/native-v2-dashboards.ts: Customer ROI, Account Engagement, Cross-sell, and Win / Loss. They intentionally contain no customer names, provider ids, SQL, cached rows, or guessed joins. After deployment, an organization owner or admin provisions them with ensure-native-v2-dashboards by supplying one real Data Program per binding key. The action validates the stored program output contract, shares the programs with the organization, and installs the read-only builtin-<template> built-ins (see Built-in Dashboards). It never touches the extension-backed originals. Do not add these to the root demo bootstrap or silently auto-bind them to guessed provider schemas.

Custom Blocks

A Custom Block is a sandboxed extension embedded as a chartType: "extension" panel (config.extensionId). It is the exception, not the default: native panels and Data Programs come first, and dual-axis charts are native (config.rightYKeys). Read custom-blocks before creating, embedding, cloning, or repairing one.

Config Shape

{
  "name": "Weekly Metrics",
  "description": "Core product and acquisition metrics",
  // Default grid columns for panels before any section. 1–6, default 2.
  // The grid is always 1 column on small screens and expands at `md:`.
  "columns": 3,
  "filters": [
    {
      "id": "date",
      "type": "date-range",
      "label": "Date Range",
      "default": "30d",
    },
  ],
  "variables": {
    "EVENTS": "`my_project.analytics.events`",
  },
  "panels": [
    // 3 metric cards sit side-by-side at md+ thanks to the dashboard's "columns": 3.
    {
      "id": "kpi-clicks",
      "title": "Clicks",
      "source": "first-party",
      "chartType": "metric",
      "width": 1,
      "config": { "timeScope": "dashboard" },
      "sql": "SELECT COUNT(*) AS value FROM analytics_events WHERE event_name = 'click' AND event_date >= '{{dateStart}}' AND event_date < '{{dateEnd}}'",
    },
    {
      "id": "kpi-signups",
      "title": "Signups",
      "source": "first-party",
      "chartType": "metric",
      "width": 1,
      "config": { "timeScope": "dashboard" },
      "sql": "SELECT COUNT(*) AS value FROM analytics_events WHERE event_name = 'signup' AND event_date >= '{{dateStart}}' AND event_date < '{{dateEnd}}'",
    },
    {
      "id": "kpi-active",
      "title": "Active users",
      "source": "first-party",
      "chartType": "metric",
      "width": 1,
      "config": { "timeScope": "dashboard" },
      "sql": "SELECT COUNT(DISTINCT user_id) AS value FROM analytics_events WHERE event_date >= '{{dateStart}}' AND event_date < '{{dateEnd}}'",
    },
    // Section header switches the grid to 2 columns for the panels below it.
    {
      "id": "trends",
      "title": "Trends",
      "chartType": "section",
      "width": 1,
      "columns": 2,
    },
    {
      "id": "events",
      "title": "Events",
      "source": "first-party",
      "chartType": "line",
      "width": 2,
      "config": { "timeScope": "dashboard" },
      "sql": "SELECT event_date AS date, COUNT(*) AS value FROM analytics_events WHERE event_date >= '{{dateStart}}' AND event_date < '{{dateEnd}}' GROUP BY 1 ORDER BY 1",
    },
  ],
}

Filters And Variables

filters[] defines dashboard-wide controls. Filter values are available in panel SQL through {{var}} interpolation. Date ranges emit {{<id>Start}} and {{<id>End}}.

For dashboard-time-bound first-party SQL, use config.timeScope: "dashboard" and a predicate that consumes the declared filter, such as event_date >= '{{dateStart}}' AND event_date < '{{dateEnd}}'. A {{timeRange}} token must have a matching select filter and SQL branches for its options; date variables must have a matching date-range filter. The server rejects unbound first-party SQL during dashboard validation.

Filter ids must be unique. Two filters with the same id collide on the same URL param, so changing one visibly updates the other in the UI. The dashboard save endpoint rejects duplicates with a 400.

Use type: "date-range" for paired start/end dates. Don't add two type: "date" filters labeled "Start" and "End" — even with distinct ids, that ships the wrong UX. A single date-range filter renders as the "from … to …" pair and exposes both halves to SQL via {{<id>Start}} / {{<id>End}}.

Use variables for shared constants such as table refs or project IDs. Identifier-like variables can be used bare (FROM {{EVENTS}}); string values should be inside SQL string literals ('{{author}}').

Use conditional blocks for optional filters:

{{?country}}AND country = '{{country}}'{{/country}}

Use type: "multi-select" for a pick list where several options can apply at once. Give it options like a select. Its value is the selected option values joined by commas. Interpolate it as IN ({{<id>:list}}), which expands to one quoted literal per selected value, and wrap the clause in a conditional so an empty selection drops the filter:

{{?plan}}AND plan IN ({{plan:list}}){{/plan}}

An unwrapped {{<id>:list}} with no selection fails the query on purpose. Option values must not contain commas.

To offer a named group of values, give the multi-select presets, such as "presets": [{ "label": "Launch apps", "values": ["clips", "slides", "design"] }]. Each preset shows as a button beside Select all in the dropdown, and picking it replaces only that filter's selection, so the other filters keep their values. Every value must be one of the filter's options. Use a preset rather than a saved view when the user wants a shortcut for one filter: a saved view replaces every filter. In Edit mode, people manage presets themselves at the bottom of the dropdown: checking two or more values that no preset matches offers Save as group, which appends the same presets entry, and a selection that matches a preset offers Rename group and Delete group for that entry. Treat a preset you did not write as someone's saved choice and keep it unless asked.

Filters auto-apply on change — there is no Apply button. Each filter change writes to the URL and re-runs the affected panels. Other filters are preserved (the URL update is functional, not destructive). If you see a filter "reset" itself when another filter changes, look for a duplicate id first.

Reset filters in the filter bar returns to the saved view that is open: the one named by view, which filter edits keep. With no view open, it returns to what a fresh load shows: the default saved view when the dashboard has one, otherwise each filter's own default. It shows only while a filter's resolved value differs from that target, so f_timeRange=30d on a 30d-default filter counts as reset. To reset for the user with set-search-params, set the target view's params and view=<id>, or, when there is no target view, remove every f_ param and view.

Filter changes are personal: they live in the URL and in the user's own dashboard-filters:<id> pref, never in the dashboard. While a filter differs from the Reset target, the bar says "Only you see these filters", and people who can edit the dashboard get Save view on hover. To share filters, save a view (shared with everyone who can open the dashboard) or send the link. The pref also remembers the open view, so a dashboard with no default view reopens on it, as long as that view still exists. A default view still wins over the pref on a fresh load.

Modifying A Dashboard

For existing dashboard edits, default to mutate-dashboard. It gives the agent a typed mutation API without exposing arbitrary JavaScript execution. Pass structured operations for normal agent calls; keep the legacy code form for short layout/config edits only. Large SQL payloads belong in the server-side first-party metric catalog, not in a prompt argument. The server parses only documented dashboard.* method calls, applies the resulting operations in memory, validates the final dashboard config, writes SQL once, syncs collab, and returns a compact result with a verified flag.

Arguments must be JSON-compatible literals, so quote object keys. Variables, imports, loops, functions, templates, network, filesystem, DB access, and calling other actions from the script are not available.

The complete typed API (dashboard.*, panel selections, PanelPatch, PanelInput, PanelFilter) is returned by mutate-dashboard called with only returnTypes: true. Subjects: dashboard.set, setFilterDefault, panel, panels, panelsMatching, section, insertPanel. Selection methods: moveToTop, moveToBottom, moveBefore, moveAfter, moveToIndex, moveNextTo, moveToRow, remove, set, setTitle, setSql, setWidth, setHeight, clearHeight, setConfig, setConfigPath, duplicate. Inserted and duplicated panels take one chained placement: atTop, atBottom, before, after, atIndex, nextTo, atRow, atRowStart, atRowEnd. Panel config holds renderer options (xKey, yKey, columns, formatters); sql, chartType, source, title, width, and height are panel fields set with setSql or set.

Examples:

dashboard.panels(["dau-over-time", "wau-over-time"]).moveToTop();
dashboard
  .panel("recurring-users-by-template")
  .duplicate("recurring-users-by-template-bar", {
    title: "Recurring Signed-In Users by Template (Bar)",
    chartType: "bar",
  })
  .nextTo("recurring-users-by-template");
dashboard.setFilterDefault("emailFilter", "exclude_builder");
dashboard.panel("top-referrers").setTitle("Top Referrers by Domain");
dashboard.panel("retention").set({
  width: 2,
  height: 2,
  config: { description: "Updated definition." },
});
dashboard.panelsMatching({ source: "first-party" }).setWidth(2);
dashboard.panel("retention").setHeight(2);
dashboard.panel("retention").clearHeight(); // return to content-based height
dashboard
  .panelsMatching({ titleIncludes: "Revenue" })
  .setConfigPath("yAxis.format", "currency");
dashboard.panelsMatching({ titleIncludes: "Signed-In" }).moveToTop();
dashboard
  .section("retention-activity-section")
  .append(["repeat-users", "retention-over-time"]);
dashboard
  .insertPanel({
    id: "new-kpi",
    title: "New KPI",
    source: "first-party",
    chartType: "metric",
    width: 1,
    config: { timeScope: "dashboard" },
    // Assumes filters includes { id: "date", type: "date-range", default: "30d" }.
    sql: "SELECT COUNT(*) AS value FROM analytics_events WHERE event_date >= '{{dateStart}}' AND event_date < '{{dateEnd}}'",
  })
  .atTop();

Native tool call:

{
  "dashboardId": "weekly-metrics",
  "code": "dashboard.panels([\"dau-over-time\",\"wau-over-time\"]).moveToTop();"
}

Use update-dashboard only for new full-config saves, UI full-config saves, or when the user specifically requests low-level JSON-pointer edits.

get-sql-dashboard is compact by default: per-panel summaries (ids, titles, chart types, sources, bindings — the config keys that name result columns — sqlChars, sqlHash, configKeys), layout groups, layout.panelOrder, layout.firstPanelIds, and a revision. Pass panelIds: ["panel-id"] to get the full SQL and config of just those panels in panelDetails; ids that do not exist come back in missingPanelIds. includeConfig: true returns every panel's SQL and config, but past ~12k characters it returns the compact summaries with truncated: true and omittedPanelIds instead, so name the panels you need with panelIds.

After a mutation, navigate to the dashboard if the user is elsewhere. The app syncs through the framework's polling/query invalidation path.

Reordering Panels

For simple "move this chart/section" requests, prefer mutate-dashboard with structured operations. The short script form is also supported for one compact move:

{
  "dashboardId": "weekly-metrics",
  "code": "dashboard.panels([\"dau-over-time\",\"wau-over-time\"]).moveToTop();"
}

Do not do index arithmetic with /panels/<index> unless the user specifically asks for a low-level JSON-pointer edit. Use moveBefore, moveAfter, moveToTop, moveToBottom, or moveToIndex against panel ids instead.

For visible placement requests like "second row" or "next to return rates", use row-aware placement: dashboard.insertPanel({...}).nextTo("retention-over-time"), .atRow(2), or dashboard.panel("panel-a").moveNextTo("panel-b"). These keep panels in the intended rendered row and rebalance that row when needed. In operations, movePanels, insertPanel, and duplicatePanel take the same placement as fields: position, index, beforePanelId, afterPanelId, nextToPanelId (same visible row, after that panel), or rowNumber (1-based visible row) with rowPosition start or end (default end).

get-sql-dashboard returns layout.panelOrder, layout.firstPanelIds, and row/group summaries. Use layout.groups[].rows[].rowNumber/panelIds to read where panels sit instead of counting positions from memory. Layout says nothing about whether a chart renders: the mutate-dashboard verification result and inspect-dashboard-panel are the proof.

Existing Dashboard Edits

When the user asks to change an existing panel on the open dashboard:

  1. Read only that panel: get-sql-dashboard with panelIds: ["<panel-id>"]. The compact bindings of every panel show which result columns its chart is bound to. The open dashboard and selected panel are in <current-screen>.
  2. Call mutate-dashboard once with every change in operations. Use panel ids, not shifted array indexes. The short code form is only for compact layout/config edits; do not stream a large multi-panel SQL script.
  3. Read the verification result before saying anything about the chart. verified is always true or false. verified: true means every panel the edit touched renders; with noRenderAffected: true the edit changed no chart (a title, width, or move), nothing needed to run, and it is done. verified: false means the edit saved but a panel is not confirmed: each unverified[] entry says why and nextStep says what to call. On verified: false, an error, or the user saying the change is not visible, call inspect-dashboard-panel for that panel, fix what it reports, and verify again. Never describe a visible change you have not verified. An edit that changes a panel's query also returns cost: what one refresh of the changed panels scans and costs at the default filters, and per month for the dashboard's email reports. The summary repeats it; when it says the cost is over a threshold, tell the user the number. update-dashboard, compose-dashboard, and restore-dashboard-revision return the same fields. Editing a dashboard you can only view fails with dashboard_forbidden before any SQL runs.

For SQL-only panel edits, use dashboard.panel("id").setSql("..."). If the metric semantics changed, also update the visible definition with setConfigPath("description", "...") or set({ "description": "..." }). If the title, source, chart type, width, or config shape changes together, put them in the same set({...}) call.

Reading Panel State

  • "No data" means the panel's resolved SQL returned zero rows: dashboard filters or {{timeRange}} bounds, a stale config.pivot, or a rewritten query that no longer returns the columns the config names.
  • A banner reading "Ignored missing result columns" means config names a column (xKey, yKey, yKeys, rightYKeys, barKeys, or a table columns[].key) the query does not return. A missing banner is not proof the chart shows what you intended.
  • Only columns listed in config.yKeys (or yKey) are plotted; an extra SQL column the config does not name never appears.
  • config.pivot ({ xKey, seriesKey, valueKey }) reshapes long-format rows into one series per seriesKey value and drops every other column. If the query now returns wide-format rows, remove pivot or the panel shows "No data".
  • A stacked bar or area chart draws its largest series at the bottom and an "Other"/"unknown" bucket on top; the legend and tooltip use the same order. To fix the order, list series values in config.pivot.seriesOrder (they go first, the rest follow by size) or list config.yKeys for wide rows.
  • config.colors is matched to series in config.pivot.seriesOrder order, then result order, never by stack position. When colors carry meaning (green for positive), list those series in seriesOrder so row order cannot swap them.
  • To split a chart by Agent-Native app, pivot by a column named template or app that holds the lowercase app key. Then each app (mail, slides, ...) takes its fixed color from shared/tokens/app-series-colors.ts, on every dashboard and in emailed reports, and the chart's other series (unknown, docs) are muted. Leave config.colors off such panels, or it replaces those colors.
  • A chart draws at most 400 points and says so under the chart. Line, area, combo and heatmap charts keep their last rows, and bar charts their first; over dates in date order (YYYY-MM-DD, YYYYMMDD, Sep 30, 2026, weeks, quarters) both keep the newest. Aggregate or limit the SQL instead of returning more.
  • Config keys the renderer does not honor are ignored. mutate-dashboard rejects them on the panels you change and lists the honored keys; read that error instead of retrying the same key.

Rolling Averages And Trend Lines

There is no native rolling or moving-average option. Add a window-function column to the panel SQL, for example AVG(value) OVER (ORDER BY week ROWS BETWEEN 3 PRECEDING AND CURRENT ROW) AS value_4wk_avg, and list it in config.yKeys. For bars plus a line, use chartType: "combo" with config.barKeys naming the bar series; the other yKeys draw as lines. With config.pivot set, extra columns are dropped, so remove pivot or emit the average as an extra series row.

First-Party User Metrics

For first-party /track events, be precise about identity:

Metric intentIdentity expression
Account users, DAU, WAU, retention, cohortsNULLIF(user_id, '') plus NULLIF(user_id, '') IS NOT NULL, but only on events that actually represent the activity being measured
Signed-in visitor activityevent_name = 'session status' AND signed_in = 'true' keyed by COALESCE(NULLIF(user_id, ''), NULLIF(anonymous_id, '')), labeled as signed-in visitors rather than account users
Public traffic, visitors, clip/share viewersCOALESCE(NULLIF(user_id, ''), NULLIF(anonymous_id, ''))

Do not call anonymous visitors "users" in dashboard labels or descriptions. When a user asks for DAU, WAU, retention, repeat users, or account cohorts, exclude logged-out traffic unless they explicitly ask for visitor metrics. If the active/session events do not include account identity, do not substitute signup or identify events and call that DAU/WAU. Either update instrumentation to send account identity on active events, or label the dashboard metric as signed-in visitor activity.

For template/app activity metrics, exclude docs from DAU, WAU, retention, and repeat-user panels. A docs event may carry signed_in = true from shared auth state or tracker context, but docs traffic is not app usage and should not appear as an app/template series. Use a minimum cohort-size threshold for retention rates so one or two identities cannot create misleading 100% or 0% spikes.

Built-in Dashboards

Dashboards that ship with the app are built-ins: served from code, read-only, and updated whenever Analytics ships. get-sql-dashboard marks one with builtin: { id, version } and canEdit: false, and list-sql-dashboards with builtin: true. They are the Node Exporter demo (builtin-demo-node-exporter, listed for everyone until they delete it), the native v2 patterns (builtin-<template>, listed once ensure-native-v2-dashboards installs them for the org), Risk Meeting (builtin-risk-meeting, installed for whoever runs ensure-risk-meeting-dashboard), and the seed dashboards at their seed ids (agent-native-templates-first-party, skills-cli-funnel, google-analytics, node-exporter-full). An install records only its inputs (bindings, program ids) in the analytics-builtin-dashboards setting, never a dashboard row.

To change a built-in, call duplicate-dashboard and edit the copy; saving at a built-in's id fails with builtin_dashboard_read_only. Don't add a built-in by writing a row from an ensure action or on app load: that copy stops updating the day it's written, which is the problem built-ins exist to solve.

Before built-ins, installs wrote per-user or per-org copies (demo-node-exporter-<hash>, native-<template>-<hash>, risk-meeting). The deploy pass in server/lib/builtin-dashboard-pass.ts (run by migrate-production) hides a copy only when its config still matches what ships and nobody has shared, viewed-and-saved, subscribed to, noted, certified, filed, archived or hidden it, and records every copy it checks in analytics_builtin_dashboard_checks. A hidden copy's id opens the built-in; every other copy stays an ordinary dashboard. scripts/_builtin-dashboards.ts takes --dry-run for counts and --restore to unhide everything it hid.

Building Large First-Party Dashboards (compose-dashboard)

For a first-party analytics dashboard, prefer compose-dashboard over hand-authoring a big update-dashboard config. You name the metrics; the SERVER expands each into a full, validated panel (SQL + chart config) from the shipped metric catalog and saves them in ONE atomic call. This avoids the failure mode where the agent must stream a giant multi-panel update-dashboard argument inside the ~40s budget — that big tool-call can't be resumed mid-stream and is all-or-nothing on validation, so the agent thrashes (repeated update-dashboard + tool-search, never landing).

  • Never hand-author large first-party configs panel-by-panel. Call compose-dashboard with the metric keys instead.
  • Unknown metric keys are skipped and reported in unknownMetrics (not fatal). Each panel's SQL is validated independently — valid panels save, invalid ones are reported in invalidMetrics.
  • By default (no overwrite), composing into an existing dashboard APPENDS the new panels and skips ids already present. overwrite: true replaces the whole config.
  • Set refreshExisting: true when refreshing a catalog-backed dashboard. Matching metric panels are replaced in place, unrelated panels and layout order are preserved, and the server returns refreshedExistingIds.
  • Each metric accepts an optional per-metric window of '30d' | '90d' | 'all' (only affects windowed virality/time metrics) and title / chartType / width overrides. Request 'all' only when the user asks for all-time coverage, and describe it as full available history.
  • Returns { saved, dashboardId, panelCount, createdMetrics, refreshedExistingIds, unknownMetrics, invalidMetrics, skippedExistingIds } — report saved: true, panelCount, and any invalid/unknown metrics as proof-of-done.

Available metric keys: total-signups, signups-over-time, signups-by-template, sessions-by-app, sessions-over-time, replay-sessions, replay-chunks-over-time, recent-replay-sessions, signed-in-vs-anon, total-template-clicks, total-demo-clicks, total-cli-copies, template-interest-over-time, clicks-by-template, demo-clicks-by-template, cli-copies-by-template, cli-copies-over-time, pageviews-over-time, top-referrer-domains, referred-signups-30d, viral-signup-share-30d, clip-share-signups-30d, signups-by-referral-source, referred-signups-over-time, top-referrers, share-funnel-30d, viral-participation-rate-90d, viral-coefficient-90d, activated-referrers-90d.

# Build a large first-party dashboard in ONE call (server generates the panels)
pnpm action compose-dashboard --dashboardId first-party-overview --title "First-Party Overview" \
  --metrics '["total-signups","signups-over-time","signups-by-template","sessions-by-app","viral-coefficient-90d","top-referrers","share-funnel-30d"]'

Reliable Bulk Edits

Hosted agent runs have a ~40s budget. Many sequential update-dashboard calls (one per panel, plus schema-discovery calls) will blow that budget and leave the dashboard in a partial state — earlier inserts looked like they succeeded (✓), but nothing actually persisted. Avoid this:

  • For a large first-party dashboard, use compose-dashboard (see the section above): name the metrics, the server generates the panels in one call. Do not hand-author the big config.
  • Batch ALL edits into ONE mutate-dashboard call. Structured operations can move, insert, remove, duplicate, and update many panels. Never loop dashboard edit actions panel-by-panel.
    • To bulk edit existing panels, use selectors: dashboard.panelsMatching({"source":"first-party"}).setWidth(2);
    • To make nested config edits, use setConfigPath("yAxis.format", "percent") instead of resending/clobbering the whole nested object.
  • Read the verification result and report it. mutate-dashboard returns verified, panelCount, appliedOps, panelOrder, firstPanelIds, changedPanelIds, commandLog, and a summary string. The tool's own echo is not proof the chart renders: only verified: true is (with noRenderAffected: true no chart changed). On verified: false, follow nextStep and call inspect-dashboard-panel for the changed panels.
  • After any timeout, read the dashboard back with get-sql-dashboard before reporting success or retrying.
# Add or edit several panels in ONE atomic call (never one call per panel)
pnpm action mutate-dashboard --dashboardId weekly-metrics \
  --code 'dashboard.panelsMatching({"source":"first-party"}).setWidth(2);'

Archiving vs deleting

Dashboards have a soft-delete state. The default user-facing destructive action is Archive (recoverable). Hard delete still exists, but lives behind a "Delete permanently" confirm in both the page header and the sidebar dropdown — and in the agent surface, behind the older delete-dashboard action. Archived rows stay in the dashboards table with archived_at set, are hidden from the default sidebar list, and remain accessible by id (so deep links in chat history keep working) until explicitly purged.

# Archive
pnpm action archive-dashboard --id weekly-metrics
# Restore
pnpm action archive-dashboard --id weekly-metrics --archived false

Default the agent to archive when the user says "delete" / "remove" / "get rid of" a dashboard. Reach for hard delete only when the user explicitly says "permanently", "for good", or similar. List queries default to active rows only — use ?archived=1 on /api/sql-dashboards (or the archived: 'all' | 'archived' | 'active' option on listDashboards) to see archived rows.

Sharing

Dashboards are private by default. Use the framework sharing actions:

pnpm action share-resource --resourceType dashboard --resourceId weekly-metrics --principalType org --principalId <org-id> --role viewer
pnpm action set-resource-visibility --resourceType dashboard --resourceId weekly-metrics --visibility org

Writes require editor access; deletes require admin access. Owners always satisfy access checks.

If a dashboard embeds an extension panel (chartType: "extension"), sharing the dashboard does not share the extension. Share the referenced extension to the same audience (share-resource --resourceType extension ...) so all dashboard viewers can see the embedded content; otherwise they get an "extension unavailable" placeholder.

Important Rules

  • Never fabricate data or create a dashboard from guessed schema. A panel's SQL must hit a real source; do not present figures you did not actually query.
  • Never write dashboard configs into the settings table.
  • Never use db-patch as a fallback for dashboard config edits. Use mutate-dashboard for existing edits, or update-dashboard for new/full config saves, and fix the action arguments.
  • Never set panel.source to a table name or unsupported backend.
  • Before saving a BigQuery or first-party panel that reads a large table, or adding an email report to a dashboard, check its cost with estimate-query-cost --dashboardId <id> (add --panelId for one panel). It is a free dry run; tell the user any panel flagged over $1 per run or $50 per month. Dashboard estimates count email reports but not alert rules: before adding an alert, estimate an analytics_events query with the alert's filters and its window's event_date and timestamp bounds, as alerts send both (the dry run skips only partitions the event_date filter rules out), using --source first-party --eventsSource raw (alerts run as first-party SQL on the raw table) and --runsPerMonth set from the poll interval (8640 for every 5 minutes).
  • Use first-party for /track data and query-agent-native-analytics for ad-hoc first-party event questions.
  • Use update-dashboard for new dashboard config saves and full config replacements. Use mutate-dashboard for existing dashboard edits.

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

コメント

ログインなしで書き込めます。使ってみた感想、質問、動かなかった環境などを気軽にどうぞ。

まだコメントはありません。最初のコメントを書いてみませんか。

送信するとすぐに公開されます。メールアドレスや電話番号などの個人情報は書かないでください。URL は 2 つまでです。

0 / 1000 文字

このフォームは reCAPTCHA で保護されており、Google のプライバシーポリシーと利用規約が適用されます。

同じリポジトリのスキル

a2a-assets

無料概要あり

複数のアプリやMCPホストからブランド画像・動画を生成、調整、書き出し、挿入する作業を支援します。ライブラリ選択や修正依頼も扱います。

  • スライド用のブランド画像を作りたいとき
  • 既存素材をフィードバックに合わせて直したいとき
  • 動画素材を生成して書き出したいとき
BuilderIO/agent-native7,5252026年10月26日 更新

account-health

無料概要あり

特定の顧客企業について、契約と製品利用の数値を照らし合わせ、根拠や不足情報を明記した健全性レポートをまとめます。

  • 顧客の健全性を確認したいとき
  • 契約席数と実利用を比べたいとき
  • 更新前に利用状況を整理したいとき
BuilderIO/agent-native7,5252026年10月26日 更新

actions

無料概要あり

アプリの操作をエージェントと画面の両方から呼び出せるアクションとして実装し、API連携や画面のデータ取得を一貫した形で追加するスキルです。

  • 新しいアプリ操作を追加したいとき
  • 外部APIと連携したいとき
  • 画面からデータを取得したいとき
BuilderIO/agent-native7,5252026年10月26日 更新

adding-a-feature

無料概要あり

新機能をUI・エージェント用アクション・説明資料・画面状態の4領域でそろえ、利用者とエージェントのどちらからも一貫して使えるようにするチェックリストです。

  • 新機能追加の作業範囲を整理したいとき
  • UIとエージェント操作をそろえたいとき
  • 画面状態の同期漏れを防ぎたいとき
BuilderIO/agent-native7,5252026年10月26日 更新

adhoc-analysis

無料概要あり

複数のデータソースを照合して、傾向や指標、根拠をまとめた分析結果を作成します。必要に応じて、再実行できるダッシュボードとして保存できます。

  • 商談と通話記録を照合したいとき
  • 顧客の利用状況を横断して調べたいとき
  • 繰り返し更新できる分析を保存したいとき
BuilderIO/agent-native7,5252026年10月26日 更新

admin-surfaces

無料概要あり

Analyticsの管理画面で、機能フラグの状態確認と切り替え、ダッシュボード利用状況の調査、接続先アプリのデータベース管理を行います。

  • アプリごとの機能フラグを確認・変更したいとき
  • ダッシュボードの所有者や利用状況を調べたいとき
  • 接続済みアプリのテーブルを管理したいとき
BuilderIO/agent-native7,5252026年10月26日 更新

BuilderIO のスキルをすべて見る

このスキルの問題を報告する