plugins/mixpanel-headless/skills/dashboard-expert/SKILL.md
Full CRUD and analysis for Mixpanel dashboards. Use when the user asks to build, create, analyze, read, understand, explain, modify, update, enhance, or manage dashboards, or asks about dashboard layout, text cards, or report arrangement. Covers dashboard analysis (read + understand existing), creation (new builds), modification (update existing), and explanation (data-driven annotation).
npx skillsauth add openai/plugins dashboard-expertInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Analyze, build, modify, and explain Mixpanel dashboards. Four modes — pick the one matching the user's intent.
| User intent | Mode | Key actions | |---|---|---| | "analyze/understand/read/explore dashboard" | Analyze | Read structure, execute reports, summarize | | "build/create/make a new dashboard" | Build | Investigate data → plan → create with layout | | "modify/update/add to/fix/improve dashboard" | Modify | Read current state → plan changes → execute | | "explain/annotate/add insights to dashboard" | Explain | Analyze → generate data-driven text cards |
import json, re
import mixpanel_headless as mp
ws = mp.Workspace()
dash = ws.get_dashboard(DASHBOARD_ID)
layout, contents = dash.layout, dash.contents
# Extract structure: rows → cells → content items
for row_id in layout["order"]:
row = layout["rows"][row_id]
for cell in row["cells"]:
cid, ctype = str(cell["content_id"]), cell["content_type"]
if ctype in ("report", "report-link"):
info = contents["report"][cid]
print(f" [{cell['width']}w] {info['name']} ({info['type']}) {'[linked]' if ctype == 'report-link' else ''}")
elif ctype == "text":
md = contents["text"][cid].get("markdown", "")
is_header = bool(re.search(r'<h2[\s>]', md, re.I))
print(f" [{cell['width']}w] TEXT {'[SECTION]' if is_header else ''}: {md[:60]}...")
# Execute each report → DataFrame
for cid, info in contents.get("report", {}).items():
btype, bid = info["type"], info["id"]
if btype == "flows":
result = ws.query_saved_flows(bid)
else:
result = ws.query_saved_report(bid, bookmark_type=btype)
df = result.df
print(f"{info['name']}: {len(df)} rows, columns={list(df.columns)}")
from mixpanel_headless.types import CreateDashboardParams, DashboardRow, DashboardRowContent
import json
ws = mp.Workspace()
dau = ws.query("Login", math="dau", last=90)
def text(html):
return DashboardRowContent(content_type="text", content_params={"markdown": html})
def report(name, btype, result):
return DashboardRowContent(content_type="report", content_params={
"bookmark": {"name": name, "type": btype, "params": json.dumps(result.params)}})
dashboard = ws.create_dashboard(CreateDashboardParams(
title="Product Health", description="Core metrics.",
rows=[
DashboardRow(contents=[text("<h2>Product Health</h2><p>Core metrics.</p>")]),
DashboardRow(contents=[report("DAU (90d)", "insights", dau)]),
],
))
ws.pin_dashboard(dashboard.id) # Make visible to team
Read existing dashboards, execute their reports, and synthesize understanding.
dash = ws.get_dashboard(dashboard_id)
layout, contents = dash.layout, dash.contents
Parse the response into a structured representation:
layout["order"] — ordered list of row IDslayout["rows"][row_id]["cells"] — cells with content_id, content_type, widthcontents["report"][str(content_id)] — report metadata: id (bookmark_id), name, type, params, descriptioncontents["text"][str(content_id)] — text card: markdownClassify each cell:
content_type == "report" → owned, editablecontent_type == "report-link" → linked from another dashboard, read-onlycontent_type == "text" → text card; detect section headers via re.search(r'<h2[\s>]', md, re.I)Build a mental model: Group reports by section (text cards with <h2> tags delimit sections). Note each report's chart type, width, and position.
For deeper understanding, fetch full bookmark params:
bookmark = ws.get_bookmark(bookmark_id)
params = bookmark.params # Full query definition dict
Key fields in params (Insights format):
params["sections"]["show"] — metrics with event names and math typeparams["sections"]["group"] — breakdown propertiesparams["sections"]["filter"] — active filtersparams["sections"]["time"] — date rangeparams["displayOptions"]["chartType"] — visualization typeNote: params in contents["report"][id] may be a JSON string — parse with json.loads() if needed.
Execute each report to get live data:
for cid, info in contents.get("report", {}).items():
bid, btype = info["id"], info["type"]
if btype == "flows":
result = ws.query_saved_flows(bid)
else:
result = ws.query_saved_report(bid, bookmark_type=btype)
df = result.df
Summarize by report type:
| Type | Key metrics to extract | |---|---| | insights | Total, average, latest value, min, max, trend direction | | funnels | Step names, counts, per-step and overall conversion rate | | retention | Day 1, Day 7, Day 30 rates; stabilization point | | flows | Top paths, conversion rate, drop-off points |
Cross-correlate across reports: Look for relationships — DAU trends vs. retention, funnel drop-off vs. feature adoption.
Structure findings as:
When analyzing multiple dashboards, build a unified picture:
dashboard_ids = [1001, 1002, 1003]
all_data = {}
for did in dashboard_ids:
dash = ws.get_dashboard(did)
for cid, info in dash.contents.get("report", {}).items():
result = ws.query_saved_report(info["id"], bookmark_type=info["type"])
all_data[f"{dash.title}/{info['name']}"] = result.df
# Cross-dashboard: join DataFrames on date index, compute correlations
Create new dashboards from scratch. Five phases.
Before building, discover the data. Never build reports for events with zero volume.
ws = mp.Workspace()
top = ws.top_events(limit=15)
for t in top:
print(f"{t.event}: {t.count:,} ({t.percent_change:+.1%})")
# Validate candidate events
for event in candidate_events:
result = ws.query(event, from_date="2025-01-01", to_date="2025-03-31")
print(f"{event}: {result.df['count'].sum():,.0f} total")
# Explore properties for breakdowns
props = ws.properties(event="key_event")
values = ws.property_values(event="key_event", property="platform", limit=20)
Present a proposed structure before building. Choose a template from references/dashboard-templates.md.
A plan includes: title + description, sections with text card headers, reports per section with chart type, grid layout.
Text cards use HTML (not markdown). Every dashboard must have an intro text card and section headers.
Allowed HTML tags: <h1>, <h2>, <h3>, <p>, <strong>, <em>, <u>, <s>, <mark>, <code>, <blockquote>, <hr>, <br>, <ul>, <ol>, <li>, <a href="...">
Forbidden (stripped): <div>, <span>, <b> (use <strong>), <i> (use <em>), <img>, <table>
Critical: Strip \n and collapse whitespace from HTML before sending. Each element renders as its own line.
Text card patterns:
Intro: <h2>Dashboard Title</h2><p>What and why. Time period: last 90 days.</p>
Section: <h2>Acquisition</h2><p>How users discover and sign up.</p>
Explainer: <p>^ Signup conversion is <strong>23.4%</strong>, up 2.1pp.</p>
Query each metric, verify data, then create with layout in one call.
def text(html):
return DashboardRowContent(content_type="text", content_params={"markdown": html})
def report(name, btype, result, description=None):
params = {"bookmark": {"name": name, "type": btype, "params": json.dumps(result.params)}}
if description:
params["bookmark"]["description"] = description
return DashboardRowContent(content_type="report", content_params=params)
dashboard = ws.create_dashboard(CreateDashboardParams(
title="Product Health Dashboard",
description="Key metrics for product health monitoring.",
rows=[
DashboardRow(contents=[text("<h2>Product Health</h2><p>Updated daily.</p>")]),
DashboardRow(contents=[
report("DAU (90d)", "insights", dau),
report("Signups (90d)", "insights", signups),
report("Revenue (90d)", "insights", revenue),
]),
DashboardRow(contents=[text("<h2>Conversion</h2><p>Key funnels.</p>")]),
DashboardRow(contents=[report("Signup Funnel", "funnels", funnel)]),
],
))
On report failure, substitute a fallback text card:
try:
result = ws.query(event, math="total", last=90)
row_items.append(report(f"{event} Trend", "insights", result))
except Exception as e:
row_items.append(text(f"<p><strong>Failed:</strong> {event} — {e}</p>"))
ws.pin_dashboard(dashboard.id) — dashboards are invisible by defaultws.favorite_dashboard(dashboard.id)references/dashboard-reference.md Section 3.4Open the dashboard and confirm all reports render with data, text cards display correctly, and layout matches the plan.
Update existing dashboards. Read first, then apply changes in the correct order.
Use Analyze Phase A1-A2 to understand the dashboard's structure. Present to user before making changes.
Classify each change and plan execution order. Operations must follow this sequence:
rows_order) — after creates so temp IDs resolveAdding content to a specific existing row — send content AND layout together:
import copy
dash = ws.get_dashboard(dashboard_id)
layout = copy.deepcopy(dash.layout)
target_row = layout["rows"][target_row_id]
# Redistribute widths
new_count = len(target_row["cells"]) + 1
cell_width = 12 // new_count
for cell in target_row["cells"]:
cell["width"] = cell_width
target_row["cells"].append({"temp_id": "-1", "width": cell_width})
ws.update_dashboard(dashboard_id, UpdateDashboardParams(
content={"action": "create", "content_type": "report",
"content_params": {"bookmark": {"name": "New Report", "type": "insights",
"params": json.dumps(result.params)}}},
layout={"rows_order": layout["order"], "rows": layout["rows"]},
))
Adding content as a new row — content action alone (appends to bottom):
ws.update_dashboard(dashboard_id, UpdateDashboardParams(
content={"action": "create", "content_type": "text",
"content_params": {"markdown": "<p>^ Explainer card.</p>"}},
))
Deleting content:
ws.update_dashboard(dashboard_id, UpdateDashboardParams(
content={"action": "delete", "content_type": "report", "content_id": content_id},
))
Cross-type updates (e.g., text → report): API rejects changing content_type on update. Delete the old cell, then create the new one.
See references/dashboard-reference.md Section 8 for temp ID resolution, operation ordering details, and report-link semantics.
Combine analysis with targeted text card insertion.
latest = df.iloc[-1]["count"]
prev = df.iloc[-8]["count"]
trend = ((latest - prev) / prev) * 100
html = (f"<p>^ DAU is <strong>{latest:,.0f}</strong>, "
f"{'up' if trend > 0 else 'down'} <strong>{abs(trend):.1f}%</strong> "
f"vs. last week.</p>").replace("\n", "")
ws.update_dashboard(dashboard_id, UpdateDashboardParams(
content={"action": "create", "content_type": "text",
"content_params": {"markdown": html}},
))
Combined content+layout PATCH — send both content and layout in the same UpdateDashboardParams to add cells to specific existing rows. Without layout, new content appends as a full-width row at the bottom.
Width auto-redistribution — when adding to an existing row with N cells, set all cells (including new) to 12 // (N+1) width.
Update operation ordering — metadata → cell creates → rows_order → cell updates → cell deletes → row deletes. Wrong order causes failures.
per_user requires math_property — using per-user aggregation without a numeric property raises BookmarkValidationError.
CreateBookmarkParams(dashboard_id=X) does NOT add to layout — use add_report_to_dashboard() or inline content action.
add_report_to_dashboard() CLONES — creates "Duplicate of..." copy. Use rows in CreateDashboardParams or inline content action instead.
GET order vs PATCH rows_order — layout from GET uses order; PATCH expects rows_order.
Never include version in layout PATCH — the API rejects it.
Strip \n and collapse whitespace — call .replace("\n", "").strip() on text card HTML. Newlines cause TipTap to mangle content.
Limits — title 255 chars, description 400 chars, text cards 2,000 chars, max 4 items/row, max 30 rows.
Cross-type cell updates require delete+create — API rejects changing content_type on an update action.
Report-link cells are read-only — content_type: "report-link" references a report owned by another dashboard. You can view but not edit its params.
Auto-pin after creation — dashboards are invisible to the team by default. Call ws.pin_dashboard(dashboard.id).
The markdown field accepts only HTML — despite the name. Markdown syntax renders as literal text.
references/dashboard-reference.md — Complete API reference, layout system, content actions, text card formatting, update operations, analysis patternsreferences/dashboard-templates.md — 9 purpose-built dashboard templates with section layouts and report specsreferences/bookmark-pipeline.md — End-to-end pipeline from typed query to dashboard report for all 4 enginesreferences/chart-types.md — Chart type selection guide with slugs, use cases, and width recommendationstools
Top-level workflow skill for USD performance diagnosis and optimization. Use for slow loading, high memory, low FPS, or 'optimize my scene' requests; delegates auth/runtime setup to Phase 0 owners.
data-ai
Use when the user mentions MagicPath, designs, UI components, themes, canvas selections, or repo-to-canvas UI work; run magicpath-ai to search, inspect, install, or author components.
documentation
Use as the top-level router for Omniverse Realtime Viewer USD app requests and focused viewer reference documents.
tools
Turn Notion specs into implementation plans, tasks, and progress tracking; use when implementing PRDs/feature specs and creating Notion plans + tasks from them.