複数のアプリやMCPホストからブランド画像・動画を生成、調整、書き出し、挿入する作業を支援します。ライブラリ選択や修正依頼も扱います。
- スライド用のブランド画像を作りたいとき
- 既存素材をフィードバックに合わせて直したいとき
- 動画素材を生成して書き出したいとき
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.
実行スコア 未検証
インストール方法を見るAnalyticsのダッシュボードを作成し、データソースとクエリを指定したパネルを配置できます。パネルの順序や幅・高さを調整し、ダッシュボードをフォルダーで整理したり、共有したりする作業も対象です。保存時には設定やクエリの検証を行い、変更履歴から以前の状態を確認・復元できます。時系列グラフには、リリースやローンチなどの重要日を示すマーカーを追加できます。計測障害などで数値を信頼できない期間は、データ注記として期間や影響対象を記録できます。新規作成では、用意されたカタログやスキーマ情報を参照し、指標の意味が数値を左右するほど曖昧な場合は確認してから構成します。
新しい分析ダッシュボードを作りたいときや、複数パネルのレイアウトをまとめて変更したいときに向いています。ローンチ日をグラフに示したり、データ異常の期間を共有したりする場面にも役立ちます。単一パネルの小さな編集には、このスキルは不要とされています。
資料は途中までのため、後半に記載された内容は反映していません。ダッシュボードの保存・共有にはAnalytics環境の権限が必要です。BigQueryをデータソースにする場合、クエリの検証と課金上限に関する設定が記載されています。
この紹介文は、公開されている SKILL.md をもとに AI(OpenAI Codex)が作成しました。正確な仕様は下の原文を確認してください。
インストールする前に、エージェントに与えられる指示の中身を確認できます。
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.
Current storage:
| Table | Purpose |
|---|---|
dashboards | Explorer and SQL dashboard records |
dashboard_folders | Personal and shared SQL dashboard folders |
dashboard_views | Saved filter presets per dashboard |
dashboard_shares | Standard framework share grants |
dashboard_folder_shares | Standard folder share grants |
dashboard_revisions | Bounded dashboard history snapshots |
analyses | Saved ad-hoc analysis records |
analysis_revisions | Bounded analysis history snapshots |
analysis_shares | Standard 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 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.list-dashboard-folders, create-dashboard-folder, and
set-dashboard-folder for folder reads and membership changes.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.
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.
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.panel.source is a backend selector, not a table name. It must be one of:
| Source | Query shape |
|---|---|
bigquery | Literal warehouse SQL. Table names belong inside the SQL string. |
ga4 | JSON descriptor for the Google Analytics Data API. |
amplitude | JSON descriptor for an Amplitude query. |
first-party | Read-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.
When the user asks for a dashboard:
<resource scope="analytics-catalog"> references (catalog-first), else one find-data. If a relevant entry exists, use its table, columns, queryTemplate, and gotchas verbatim.ask-question clarifying tool once before building. Skip it when the dictionary or the user already settled it.search-bigquery-schema (or the provider's own schema action), then propose a dictionary entry with save-data-dictionary-entry.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.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").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.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.
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.
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.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.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:
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.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.
{ 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.config.events draws once.mutate-dashboard rejects malformed events and other chart types.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.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.setDashboard({ annotations: { hiddenDataNotes: [{ noteId, panelId }, ...] } }),
listing the notes already hidden that stay hidden; hiddenKeyDateSets is kept.When someone asks to mark something on a chart, pick the kind from what a reader should take away:
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.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.
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:
| Pattern | Native composition |
|---|---|
| Customer ROI / value realization | metric KPI cards, line or area trends, table detail, and callout or section panels for the business narrative |
| Account engagement / outreach | metric coverage and adoption cards, a daily line trend, heatmap or table segmentation, and callout alerts |
| GTM pipeline / cross-sell | funnel for ordered stages, metric totals, bar or line trends, and a table for account-level follow-up |
| Win/loss analysis | section 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.
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.
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.
{
"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[] 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.
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.
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.
When the user asks to change an existing panel on the open dashboard:
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>.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.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.
{{timeRange}} bounds, a stale config.pivot, or a rewritten query
that no longer returns the columns the config names.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.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".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.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.YYYY-MM-DD, YYYYMMDD, Sep 30, 2026, weeks, quarters) both
keep the newest. Aggregate or limit the SQL instead of returning more.mutate-dashboard
rejects them on the panels you change and lists the honored keys; read that
error instead of retrying the same key.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.
For first-party /track events, be precise about identity:
| Metric intent | Identity expression |
|---|---|
| Account users, DAU, WAU, retention, cohorts | NULLIF(user_id, '') plus NULLIF(user_id, '') IS NOT NULL, but only on events that actually represent the activity being measured |
| Signed-in visitor activity | event_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 viewers | COALESCE(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.
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.
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).
compose-dashboard with the metric keys instead.unknownMetrics (not fatal). Each panel's SQL is validated independently — valid panels save, invalid ones are reported in invalidMetrics.overwrite), composing into an existing dashboard APPENDS the new panels and skips ids already present. overwrite: true replaces the whole config.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.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.{ 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"]'
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:
compose-dashboard (see the section above): name the metrics, the server generates the panels in one call. Do not hand-author the big config.mutate-dashboard call. Structured operations can move,
insert, remove, duplicate, and update many panels. Never loop dashboard edit
actions panel-by-panel.
dashboard.panelsMatching({"source":"first-party"}).setWidth(2);setConfigPath("yAxis.format", "percent") instead of resending/clobbering
the whole nested object.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.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);'
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.
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.
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.panel.source to a table name or unsupported backend.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).first-party for /track data and query-agent-native-analytics for ad-hoc first-party event questions.update-dashboard for new dashboard config saves and full config
replacements. Use mutate-dashboard for existing dashboard edits.まだレビューはありません。使ってみた感想をお寄せください。
ログインなしで書き込めます。使ってみた感想、質問、動かなかった環境などを気軽にどうぞ。
まだコメントはありません。最初のコメントを書いてみませんか。
複数のアプリやMCPホストからブランド画像・動画を生成、調整、書き出し、挿入する作業を支援します。ライブラリ選択や修正依頼も扱います。
特定の顧客企業について、契約と製品利用の数値を照らし合わせ、根拠や不足情報を明記した健全性レポートをまとめます。
アプリの操作をエージェントと画面の両方から呼び出せるアクションとして実装し、API連携や画面のデータ取得を一貫した形で追加するスキルです。
新機能をUI・エージェント用アクション・説明資料・画面状態の4領域でそろえ、利用者とエージェントのどちらからも一貫して使えるようにするチェックリストです。
複数のデータソースを照合して、傾向や指標、根拠をまとめた分析結果を作成します。必要に応じて、再実行できるダッシュボードとして保存できます。
Analyticsの管理画面で、機能フラグの状態確認と切り替え、ダッシュボード利用状況の調査、接続先アプリのデータベース管理を行います。