Dashboard Expert
OpenAIOfficialCreated by MixpanelDistributed by OpenAI
- Skill
- Tool integration
- Data and AI
- MIT
- No issues found
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
Dashboard Expert provides a comprehensive framework for interacting with Mixpanel dashboards. It supports four primary modes: analyzing existing dashboards to extract insights, building new dashboards from scratch, modifying existing layouts or content, and adding data-driven annotations to provide context. The skill emphasizes a structured workflow, ensuring that data is investigated before building, and that modifications follow strict API ordering requirements to maintain dashboard integrity.
What it does
- Analyzes dashboard structure and executes underlying reports to synthesize findings.
- Builds new dashboards by planning layouts and querying metrics.
- Modifies existing dashboards by adding, reordering, or deleting content.
- Annotates dashboards with data-driven text cards to provide context.
- Validates data availability before creating new reports.
- Ensures correct API operation ordering for dashboard updates.
When to use it
- When you need to understand the performance metrics displayed on a dashboard.
- When creating a new dashboard to track product health or specific user behaviors.
- When updating an existing dashboard to include new metrics or improve layout.
- When you need to add explanatory text to clarify data trends for stakeholders.
Advantages
- Provides a consistent, repeatable process for dashboard management.
- Reduces errors by enforcing correct API operation sequences.
- Enables data-driven insights through automated report execution and summarization.
- Supports complex layout management including row and cell organization.
Usage examples
- “Analyze the Product Health dashboard and summarize the key trends.”
- The agent will read the dashboard structure, execute all contained reports, and provide a summary of metrics, trends, and cross-report correlations.
- “Create a new dashboard for tracking signup conversion.”
- The agent will investigate relevant events, propose a layout, and build a new dashboard with appropriate funnel and insight reports.
- “Add a text card to the top of my dashboard explaining the recent drop in DAU.”
- The agent will analyze the current dashboard, generate an insightful text card based on the data, and insert it into the layout.
Install
Download the skill as a ZIP:
https://mp-staging.autogineer.com/agent-skills/openai/dashboard-expert/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip dashboard-expert.zip -d .agents/skills
Documentation by Mixpanel
Dashboard Expert
Analyze, build, modify, and explain Mixpanel dashboards. Four modes — pick the one matching the user's intent.
Mode Selection
| 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 |
Quick Start: Analyze an Existing Dashboard
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)}")Quick Start: Build a New Dashboard
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 teamMode: Analyze
Read existing dashboards, execute their reports, and synthesize understanding.
Phase A1: Read Dashboard Structure
dash = ws.get_dashboard(dashboard_id)
layout, contents = dash.layout, dash.contentsParse the response into a structured representation:
layout["order"]— ordered list of row IDslayout["rows"][row_id]["cells"]— cells withcontent_id,content_type,widthcontents["report"][str(content_id)]— report metadata:id(bookmark_id),name,type,params,descriptioncontents["text"][str(content_id)]— text card:markdown
Classify each cell:
content_type == "report"→ owned, editablecontent_type == "report-link"→ linked from another dashboard, read-onlycontent_type == "text"→ text card; detect section headers viare.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.
Phase A2: Extract Report Details
For deeper understanding, fetch full bookmark params:
bookmark = ws.get_bookmark(bookmark_id)
params = bookmark.params # Full query definition dictKey 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 type
Note: params in contents["report"][id] may be a JSON string — parse with json.loads() if needed.
Phase A3: Execute and Summarize
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.dfSummarize 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.
Phase A4: Present Analysis
Structure findings as:
Dashboard overview — title, purpose, section count, report count
Section-by-section breakdown — what each section measures, key findings
Cross-metric insights — correlations, anomalies, patterns
Suggestions — missing metrics, better chart types, layout improvements
Multi-Dashboard Analysis
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 correlationsMode: Build
Create new dashboards from scratch. Five phases.
Phase B1: Investigate
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)Phase B2: Plan Structure
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>Phase B3: Query and Build
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>"))Phase B4: Enhance
Pin for team visibility:
ws.pin_dashboard(dashboard.id)— dashboards are invisible by defaultFavorite for personal use:
ws.favorite_dashboard(dashboard.id)Add explainer cards: see Mode: Explain
Adjust heights: see
references/dashboard-reference.mdSection 3.4
Phase B5: Verify
Open the dashboard and confirm all reports render with data, text cards display correctly, and layout matches the plan.
Mode: Modify
Update existing dashboards. Read first, then apply changes in the correct order.
Phase M1: Read Current State
Use Analyze Phase A1-A2 to understand the dashboard's structure. Present to user before making changes.
Phase M2: Plan Changes
Classify each change and plan execution order. Operations must follow this sequence:
Metadata (title/description) — standalone PATCH
Cell creates — add new content first
Row reorder (
rows_order) — after creates so temp IDs resolveCell updates — modify existing content
Cell deletes — remove content
Row deletes — remove entire rows last
Phase M3: Execute Changes
Adding 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.
Mode: Explain
Combine analysis with targeted text card insertion.
Analyze — run Mode: Analyze to extract structure and execute reports
Generate insights — for each report, compute key metrics from the DataFrame:
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", "")Insert cards — add as new rows below each report section:
ws.update_dashboard(dashboard_id, UpdateDashboardParams( content={"action": "create", "content_type": "text", "content_params": {"markdown": html}}, ))
Critical Gotchas
Combined content+layout PATCH — send both
contentandlayoutin the sameUpdateDashboardParamsto add cells to specific existing rows. Withoutlayout, 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_userrequiresmath_property— using per-user aggregation without a numeric property raisesBookmarkValidationError.CreateBookmarkParams(dashboard_id=X)does NOT add to layout — useadd_report_to_dashboard()or inline content action.add_report_to_dashboard()CLONES — creates "Duplicate of..." copy. UserowsinCreateDashboardParamsor inline content action instead.GET
ordervs PATCHrows_order— layout from GET usesorder; PATCH expectsrows_order.Never include
versionin layout PATCH — the API rejects it.Strip
\nand 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_typeon 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
markdownfield accepts only HTML — despite the name. Markdown syntax renders as literal text.
See Also
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 recommendations
Files
- ATTRIBUTION.mdAdded by AutoGineer937 B
- LICENSELicence file added by AutoGineer1.0 KiB
- SKILL.md15.4 KiB
- references/bookmark-pipeline.md9.0 KiB
- references/chart-types.md7.3 KiB
- references/dashboard-reference.md37.1 KiB
- references/dashboard-templates.md35.1 KiB
references/dashboard-reference.md
Dashboard Reference
Exhaustive technical reference for Mixpanel dashboard creation, layout, content actions, text card formatting, time filters, and known gotchas. Consult this when you need exact parameter names, field types, API behaviors, or formatting rules.
All code examples use the mixpanel_headless Python library.
import mixpanel_headless as mp
from mixpanel_headless.types import (
CreateDashboardParams,
UpdateDashboardParams,
UpdateTextCardParams,
UpdateReportLinkParams,
Dashboard,
)
ws = mp.Workspace()1. API Reference
1.1 Types
CreateDashboardParams
Import: from mixpanel_headless.types import CreateDashboardParams
| Field | Type | Default | Constraints |
|---|---|---|---|
title | str | required | Max 255 chars |
description | str | None | None | Max 400 chars |
is_private | bool | None | None (False) | |
is_restricted | bool | None | None (False) | |
filters | list[Any] | None | None | Dashboard-level filters |
breakdowns | list[Any] | None | None | Dashboard-level breakdowns |
time_filter | Any | None | None | Dashboard-level time filter (see Section 5) |
duplicate | int | None | None | ID of dashboard to duplicate |
params = CreateDashboardParams(
title="Product Health Dashboard",
description="Key metrics for monitoring product health.",
)UpdateDashboardParams
Import: from mixpanel_headless.types import UpdateDashboardParams
All fields are optional. Only provided fields are sent to the API.
| Field | Type | Default | Constraints |
|---|---|---|---|
title | str | None | None | Max 255 chars |
description | str | None | None | Max 400 chars |
is_private | bool | None | None | |
is_restricted | bool | None | None | |
filters | list[Any] | None | None | Dashboard-level filters |
breakdowns | list[Any] | None | None | Dashboard-level breakdowns |
time_filter | Any | None | None | Dashboard-level time filter |
layout | Any | None | None | Layout patch (see Section 3) |
content | Any | None | None | Content action (see Section 2) |
params = UpdateDashboardParams(title="Q1 Metrics v2")
data = params.model_dump(exclude_none=True)
# {"title": "Q1 Metrics v2"}UpdateTextCardParams
Import: from mixpanel_headless.types import UpdateTextCardParams
| Field | Type | Default | Notes |
|---|---|---|---|
markdown | str | None | None | HTML content for the text card (see Section 4) |
Model config: extra="allow" -- additional fields are preserved.
params = UpdateTextCardParams(markdown="<h2>Section Title</h2><p>Description.</p>")UpdateReportLinkParams
Import: from mixpanel_headless.types import UpdateReportLinkParams
| Field | Type | Default | Notes |
|---|---|---|---|
link_type | str | required | Serialized as "type" via alias. Example: "embedded" |
Model config: populate_by_name=True, extra="allow".
params = UpdateReportLinkParams(link_type="embedded")
data = params.model_dump(by_alias=True, exclude_none=True)
# {"type": "embedded"}Dashboard (response model)
Import: from mixpanel_headless.types import Dashboard
The Dashboard model uses extra="allow" so new API fields are preserved automatically. Key fields:
| Field | Type | Description |
|---|---|---|
id | int | Unique dashboard identifier |
title | str | Dashboard title |
description | str | None | Dashboard description |
is_private | bool | Whether dashboard is private |
is_restricted | bool | Whether access is restricted |
creator_id | int | None | Creator's user ID |
creator_name | str | None | Creator's name |
creator_email | str | None | Creator's email |
created | datetime | str | None | Creation timestamp |
modified | datetime | str | None | Last modification timestamp |
is_favorited | bool | None | Whether current user favorited it |
pinned_date | str | None | Date pinned, if any |
layout_version | str | None | Layout version metadata |
unique_view_count | int | None | Number of unique viewers |
total_view_count | int | None | Total view count |
The layout field (from get_dashboard) contains the full layout structure described in Section 3.
1.2 Workspace Methods
List dashboards
def list_dashboards(self, *, ids: list[int] | None = None) -> list[Dashboard]Retrieves all dashboards visible to the authenticated user, optionally filtered by specific IDs.
dashboards = ws.list_dashboards()
dashboards = ws.list_dashboards(ids=[101, 102, 103])Create dashboard
def create_dashboard(self, params: CreateDashboardParams) -> DashboardCreates a new dashboard. Returns the newly created Dashboard.
dashboard = ws.create_dashboard(CreateDashboardParams(title="Q1 Metrics"))
print(dashboard.id) # Use this ID for all subsequent operationsGet dashboard
def get_dashboard(self, dashboard_id: int) -> DashboardRetrieves a single dashboard by ID. The response includes the full layout structure with row IDs, cell IDs, and content IDs needed for layout patching.
dash = ws.get_dashboard(dashboard_id)
# Access layout: dash.layout["order"], dash.layout["rows"]Update dashboard
def update_dashboard(self, dashboard_id: int, params: UpdateDashboardParams) -> DashboardUpdates an existing dashboard. Only provided fields are modified. Returns the updated Dashboard.
Used for three distinct operations:
Metadata updates -- title, description, privacy, time filter
Content actions -- add/remove/update reports and text cards (via
contentfield)Layout patches -- rearrange rows, resize cells (via
layoutfield)
# Metadata update
ws.update_dashboard(dashboard_id, UpdateDashboardParams(title="New Title"))
# Content action (see Section 2)
ws.update_dashboard(dashboard_id, UpdateDashboardParams(content={...}))
# Layout patch (see Section 3)
ws.update_dashboard(dashboard_id, UpdateDashboardParams(layout={...}))Delete dashboard
def delete_dashboard(self, dashboard_id: int) -> NonePermanently deletes a dashboard. No return value.
Bulk delete dashboards
def bulk_delete_dashboards(self, ids: list[int]) -> NoneDeletes multiple dashboards in a single API call.
ws.bulk_delete_dashboards([101, 102, 103])Favorite / unfavorite dashboard
def favorite_dashboard(self, dashboard_id: int) -> None
def unfavorite_dashboard(self, dashboard_id: int) -> NoneToggles the current user's favorite status on a dashboard. Favorited dashboards appear in the user's favorites list in the Mixpanel UI.
Pin / unpin dashboard
def pin_dashboard(self, dashboard_id: int) -> None
def unpin_dashboard(self, dashboard_id: int) -> NonePinned dashboards appear at the top of the dashboard list for all project members.
Add report to dashboard
def add_report_to_dashboard(self, dashboard_id: int, bookmark_id: int) -> DashboardCLONES the specified bookmark onto the dashboard. The original bookmark is unchanged. The cloned report gets a new content ID and appears with a "Duplicate of ..." name prefix. Returns the updated Dashboard.
Prefer inline content actions (Section 2) over this method to avoid cloning behavior.
updated_dash = ws.add_report_to_dashboard(dashboard_id, bookmark_id)Remove report from dashboard
def remove_report_from_dashboard(self, dashboard_id: int, bookmark_id: int) -> DashboardRemoves a report from a dashboard by its bookmark/report ID. Returns the updated Dashboard.
ws.remove_report_from_dashboard(dashboard_id, report_id)Update text card
def update_text_card(self, dashboard_id: int, text_card_id: int, params: UpdateTextCardParams) -> NoneUpdates a text card's content. The text_card_id is the content ID from the layout.
ws.update_text_card(dashboard_id, text_card_id, UpdateTextCardParams(
markdown="<h2>Updated Title</h2><p>New description.</p>"
))Update report link
def update_report_link(self, dashboard_id: int, report_link_id: int, params: UpdateReportLinkParams) -> NoneUpdates a report link on a dashboard.
ws.update_report_link(dashboard_id, report_link_id, UpdateReportLinkParams(link_type="embedded"))2. Content Actions
Content actions add, remove, and modify items on a dashboard via the content field of UpdateDashboardParams. Each action is a dictionary with action, content_type, and action-specific fields.
Content types
| Type | Description |
|---|---|
report | A report card (insights, funnels, retention, flows) |
text | A text/markdown card |
Actions
| Action | Description |
|---|---|
create | Add new content to the dashboard |
delete | Remove content from the dashboard |
update | Modify existing content in place |
move | Move content to a different position (prefer layout PATCH — Section 3 — instead) |
duplicate | Duplicate content within the dashboard |
undelete | Restore previously deleted content |
2.1 Create text card
ws.update_dashboard(dashboard_id, UpdateDashboardParams(
content={
"action": "create",
"content_type": "text",
"content_params": {
"markdown": "<h2>Section Title</h2><p>Brief description of this section.</p>"
},
}
))The new text card is appended to the bottom of the dashboard. Rearrange via layout patch (Section 3) afterward.
2.2 Create report from existing bookmark (clones it)
ws.update_dashboard(dashboard_id, UpdateDashboardParams(
content={
"action": "create",
"content_type": "report",
"content_params": {
"source_bookmark_id": bookmark_id
},
}
))This clones the bookmark onto the dashboard. The original bookmark is unchanged. The clone gets a new content ID.
2.3 Create report inline (preferred)
import json
ws.update_dashboard(dashboard_id, UpdateDashboardParams(
content={
"action": "create",
"content_type": "report",
"content_params": {
"bookmark": {
"name": "Daily Active Users",
"type": "insights",
"params": json.dumps(result.params),
"description": "DAU trend over the last 90 days.",
}
},
}
))This is the preferred method. It creates the report directly on the dashboard without cloning, without creating a separate bookmark entity, and without the "Duplicate of ..." name prefix.
Supported type values: "insights", "funnels", "retention", "flows"
The params field must be a JSON string (use json.dumps()), not a dict.
2.4 Delete content
ws.update_dashboard(dashboard_id, UpdateDashboardParams(
content={
"action": "delete",
"content_type": "report", # or "text"
"content_id": content_id,
}
))The content_id is the numeric ID from the layout cells.
2.5 Update text card content
ws.update_dashboard(dashboard_id, UpdateDashboardParams(
content={
"action": "update",
"content_type": "text",
"content_id": text_card_id,
"content_params": {
"markdown": "<h2>Updated Title</h2><p>New description.</p>"
},
}
))2.6 Duplicate content
ws.update_dashboard(dashboard_id, UpdateDashboardParams(
content={
"action": "duplicate",
"content_type": "report",
"content_id": content_id,
}
))2.7 Undelete content
ws.update_dashboard(dashboard_id, UpdateDashboardParams(
content={
"action": "undelete",
"content_type": "report",
"content_id": content_id,
}
))3. Layout System
3.1 Grid Fundamentals
12 columns per row (
ROW_TOTAL_WIDTH = 12)Max 4 items per row
Max 30 rows per dashboard
Layout version:
"2.0.0"Standard widths: 3 (quarter), 4 (third), 6 (half), 12 (full)
Cell widths in a row must sum to exactly 12
3.2 Layout Structure from GET
When you call ws.get_dashboard(id), the layout is returned in this structure:
{
"version": "2.0.0",
"order": ["row-abc-123", "row-def-456"],
"rows": {
"row-abc-123": {
"height": 0,
"cells": [
{
"id": "cell-aaa-111",
"width": 12,
"content_id": 90001,
"content_type": "text"
}
]
},
"row-def-456": {
"height": 336,
"cells": [
{
"id": "cell-bbb-222",
"width": 4,
"content_id": 90002,
"content_type": "report"
},
{
"id": "cell-ccc-333",
"width": 4,
"content_id": 90003,
"content_type": "report"
},
{
"id": "cell-ddd-444",
"width": 4,
"content_id": 90004,
"content_type": "report"
}
]
}
}
}Key fields:
| Field | Description |
|---|---|
version | Always "2.0.0". Never include in PATCH. |
order | Ordered list of row IDs. Called order in GET response. |
rows | Dict mapping row ID to {height, cells}. |
rows[].height | Row height in pixels. 0 means auto (text-only rows). |
rows[].cells | Ordered list of cells in the row. |
cells[].id | Cell identifier (UUID string). |
cells[].width | Cell width (1-12). Sum of all cells in a row must equal 12. |
cells[].content_id | Numeric ID of the content item. |
cells[].content_type | One of: "text", "report". |
3.3 Layout PATCH Format
When patching layout via UpdateDashboardParams(layout=...), use this format. Critical: rows must be a list (not a dict). The GET response returns rows as a dict keyed by row ID, but the PATCH expects a list with id on each row object.
{
"rows_order": ["row-abc-123", "row-def-456"],
"rows": [
{
"id": "row-abc-123",
"height": 0,
"cells": [
{
"id": "cell-aaa-111",
"width": 12,
"content_id": 90001,
"content_type": "text"
}
]
},
{
"id": "row-def-456",
"height": 418,
"cells": [
{
"id": "cell-bbb-222",
"width": 6,
"content_id": 90002,
"content_type": "report"
},
{
"id": "cell-ccc-333",
"width": 6,
"content_id": 90003,
"content_type": "report"
}
]
}
]
}Critical differences from GET:
| GET response | PATCH payload |
|---|---|
"order" | "rows_order" |
"rows" is a dict keyed by row ID | "rows" is a list with "id" on each row |
Includes "version" | Never include "version" |
3.4 Complete Layout Patch Example
# 1. Get current layout to discover IDs
dash = ws.get_dashboard(dashboard_id)
layout = dash.layout # dict with version, order, rows
# 2. Extract existing row/cell IDs and content IDs
# (IDs are auto-generated UUIDs; you must read them, not invent them)
# 3. Build the patch — rows_order (not order), rows as LIST (not dict)
row_ids = list(layout["order"]) # preserve current order, or rearrange
rows_list = []
for row_id in row_ids:
row = layout["rows"][row_id]
rows_list.append({
"id": row_id,
"height": row.get("height", 0),
"cells": row.get("cells", []),
})
ws.update_dashboard(dashboard_id, UpdateDashboardParams(
layout={"rows_order": row_ids, "rows": rows_list}
))3.5 Width Assignment Table
| Content | Width | Rationale |
|---|---|---|
| Text card (section header) | 12 | Always full width |
| KPI metric card | 3 or 4 | Pack 3-4 per row |
| Line/bar chart (paired) | 6 | Side-by-side comparison |
| Line/bar chart (solo) | 12 | Full width for detail |
| Table | 12 | Needs full width for columns |
| Funnel (3+ steps) | 12 | Complex funnels need space |
| Retention curve | 12 | Full width for cohort grid |
| Sankey/flow | 12 | Always full width |
3.6 Height Guidelines
| Row Configuration | Height (px) | Notes |
|---|---|---|
| Text-only row | 0 | Auto height, expands to fit content |
| KPI row (3-4 metric cards) | 336 | Compact number display |
| Two charts side-by-side (6+6) | 418 | Standard chart height |
| Single full-width chart | 500 | More vertical space for detail |
| Full-width funnel or table | 588 | Extra space for steps/rows |
Heights are guidelines. The Mixpanel UI allows manual resizing. Use 0 for text-only rows to let them auto-size.
4. Text Card Formatting
Text cards use a restricted subset of HTML. Mixpanel's frontend renders them through a TipTap editor which sanitizes input and only supports specific tags.
4.1 Allowed HTML Tags
| Category | Tags | Notes |
|---|---|---|
| Headings | <h1>, <h2>, <h3> | Prefer <h2> and <h3>. Avoid <h1> (too large for dashboard cards). |
| Text | <p>, <strong>, <em>, <u>, <s>, <mark>, <code> | Use <strong> not <b>. Use <em> not <i>. |
| Structure | <blockquote>, <hr>, <br> | <blockquote> renders as indented block with left border. |
| Lists | <ul>, <ol>, <li> | Nested lists are supported. |
| Links | <a href="..."> | External links open in new tab. |
4.2 Tags That Will Be STRIPPED
These tags are silently removed by the sanitizer. Your content will render without them, potentially breaking layout:
| Tag | Reason | Use Instead |
|---|---|---|
<div> | Stripped by sanitizer | <p> |
<span> | Stripped by sanitizer | Inline formatting tags (<strong>, <em>, etc.) |
<b> | Non-semantic | <strong> |
<i> | Non-semantic | <em> |
<img> | Not supported | — |
<table>, <tr>, <td>, <th> | Not supported | Use a report card with table chart type |
Any inline style attributes | Stripped | Use semantic tags |
Any class attributes | Stripped | Use semantic tags |
4.3 Formatting Rules
Strip all
\nnewlines and collapse whitespace before sending. Mixpanel's TipTap editor takes a markdown-it code path that mangles HTML when newlines are present. Always call.replace("\n", "").strip()on the markdown string. Multiple spaces or tabs can also cause rendering issues.Each HTML element renders as a new line in the card. A
<p>tag produces one visual line. Two consecutive<p>tags produce two lines. Do not try to use\nfor line breaks.Character limit: Practical limit is 2,000 characters. Keep text cards under 500 characters for readability. Dashboard cards have limited vertical space.
No Markdown syntax. Despite the field being called
markdown, it accepts only HTML. Do not send# Headingor**bold**-- use<h2>Heading</h2>and<strong>bold</strong>.Section header pattern:
<h2>Section Title</h2><p>One sentence description.</p>-- keep section titles to 2-4 words.Explainer pattern:
<p>^ Brief data-driven insight about the chart above.</p>-- the^caret is a visual convention indicating this card explains the chart directly above it.
4.4 Text Card Templates
Dashboard intro
markdown = (
"<h2>Product Health Dashboard</h2>"
"<p>Core metrics for monitoring product health. Updated daily.</p>"
)Section header
markdown = (
"<h2>Growth Trends</h2>"
"<p>User acquisition and activation metrics over time.</p>"
)Explainer card (data-driven)
markdown = (
f"<p>^ DAU is <strong>{latest_dau:,.0f}</strong>, "
f"{'up' if trend > 0 else 'down'} "
f"<strong>{abs(trend):.1f}%</strong> vs. last week.</p>"
).replace("\n", "")Methodology note
markdown = (
"<p><em>Methodology:</em> DAU counts unique users who triggered "
"any event in a calendar day. Excludes bot traffic.</p>"
)Key takeaway
markdown = (
"<h3>Key Takeaway</h3>"
"<p>Mobile conversion is <strong>2.3x higher</strong> than desktop. "
"Prioritize mobile onboarding improvements.</p>"
)Warning / caveat
markdown = (
"<p><strong>Note:</strong> Data prior to Jan 15 reflects the old "
"tracking schema. Direct comparison across that boundary is unreliable.</p>"
)Bullet list summary
markdown = (
"<h3>Q1 Highlights</h3>"
"<ul>"
"<li>DAU grew <strong>18%</strong> quarter-over-quarter</li>"
"<li>Signup funnel conversion improved from 12% to 15%</li>"
"<li>7-day retention stable at <strong>42%</strong></li>"
"</ul>"
)Blockquote callout
markdown = (
"<blockquote>This dashboard covers the core product loop: "
"acquisition, activation, retention. For revenue metrics, "
"see the Revenue Dashboard.</blockquote>"
)4.5 Sending Text Card Content
Always strip newlines before sending:
# Build multi-line for readability in code
markdown = (
"<h2>Section Title</h2>"
"<p>Description paragraph one.</p>"
"<p>Description paragraph two.</p>"
)
# Strip newlines (critical -- TipTap mangles HTML with newlines)
clean_markdown = markdown.replace("\n", "")
# Send via content action
ws.update_dashboard(dashboard_id, UpdateDashboardParams(
content={
"action": "create",
"content_type": "text",
"content_params": {"markdown": clean_markdown},
}
))
# Or update existing text card directly
ws.update_text_card(dashboard_id, text_card_id, UpdateTextCardParams(
markdown=clean_markdown
))5. Dashboard Time Filters
Dashboard-level time filters override individual report time ranges. Set them at creation time or via update.
5.1 "Last N days" (rolling window)
time_filter = {
"dateRange": {
"type": "in the last",
"window": {"unit": "day", "value": 30}
},
"displayText": "Last 30 days"
}
dashboard = ws.create_dashboard(CreateDashboardParams(
title="Rolling 30-Day Metrics",
time_filter=time_filter,
))Window units: "day", "week", "month"
5.2 "Since date" (open-ended)
time_filter = {
"dateRange": {
"type": "since",
"from_date": "2025-01-01"
},
"displayText": "Since Jan 1, 2025"
}
dashboard = ws.create_dashboard(CreateDashboardParams(
title="2025 Metrics",
time_filter=time_filter,
))5.3 "Between dates" (fixed range)
time_filter = {
"dateRange": {
"type": "between",
"from_date": "2025-01-01",
"to": "2025-03-31"
},
"displayText": "Jan 1 - Mar 31, 2025"
}
dashboard = ws.create_dashboard(CreateDashboardParams(
title="Q1 2025 Metrics",
time_filter=time_filter,
))5.4 Updating time filter on existing dashboard
ws.update_dashboard(dashboard_id, UpdateDashboardParams(
time_filter={
"dateRange": {
"type": "in the last",
"window": {"unit": "week", "value": 4}
},
"displayText": "Last 4 weeks"
}
))5.5 Removing time filter
# Set to empty dict or None to remove dashboard-level time filter
ws.update_dashboard(dashboard_id, UpdateDashboardParams(time_filter={}))5.6 Dashboard-Level Filters and Breakdowns
CreateDashboardParams accepts filters and breakdowns fields. These apply globally across all reports on the dashboard. In practice, dashboard-level filters are primarily configured through the Mixpanel UI. The API fields are useful for:
Preserving existing filters when updating a dashboard programmatically
Reading filters from
get_dashboard()to understand the dashboard's scope
# Read existing dashboard-level filters
dash = ws.get_dashboard(dashboard_id)
print(dash.filters) # list of filter dicts, or None
print(dash.breakdowns) # list of breakdown dicts, or None6. Critical Gotchas
6.1 CreateBookmarkParams(dashboard_id=X) does NOT add to layout
The dashboard_id field on CreateBookmarkParams exists but only sets internal metadata. It does not place the report on the dashboard layout. You must use one of:
ws.add_report_to_dashboard(dashboard_id, bookmark_id)(clones the bookmark)Inline content action via
UpdateDashboardParams(content=...)(preferred)
6.2 add_report_to_dashboard() CLONES the bookmark
This method creates a "Duplicate of ..." copy of the bookmark. The original bookmark is unchanged. The cloned report gets a new content_id on the dashboard. To avoid this:
Use inline report creation (Section 2.3) instead
Track the returned
Dashboardto find the new content ID
6.3 Inline report creation is preferred
Use the content_params.bookmark pattern (Section 2.3) to create reports directly on the dashboard. Benefits:
No separate bookmark entity created
No "Duplicate of ..." name prefix
No need to track cloned IDs
Single API call
6.4 GET order vs PATCH rows_order
The layout returned by get_dashboard() uses "order" for the row ordering key. When sending a layout patch via update_dashboard(), the key must be "rows_order". Using "order" in a patch will silently fail to reorder rows.
6.5 Never include version in layout PATCH
The layout from GET includes "version": "2.0.0". Do not include this key in the PATCH payload. The API rejects layout patches that contain the version key.
6.6 Content IDs change on clone
When using add_report_to_dashboard() or the source_bookmark_id content action, the new report gets a new content_id. Always read the response to discover the real ID. Do not assume the content ID matches the source bookmark ID.
6.7 Strip newlines from text card markdown
Always call .replace("\n", "") on markdown strings before sending. Mixpanel's TipTap editor takes a markdown-it code path that mangles HTML when newlines are present. This produces garbled rendering in the dashboard UI.
# WRONG -- newlines in the string
markdown = "<h2>Title</h2>\n<p>Description</p>"
# CORRECT -- no newlines
markdown = "<h2>Title</h2><p>Description</p>"
# CORRECT -- use Python string concatenation for readability
markdown = (
"<h2>Title</h2>"
"<p>Description</p>"
)6.8 Character and length limits
| Constraint | Limit |
|---|---|
| Dashboard title | Max 255 characters |
| Dashboard description | Max 400 characters |
| Cell widths per row | Must sum to exactly 12 |
| Items per row | Max 4 |
| Rows per dashboard | Max 30 |
| Nested dashboard depth | Max 2 levels |
6.9 Cell widths must sum to 12
Standard width values are 3, 4, 6, and 12. Any combination is allowed as long as widths in a row sum to exactly 12. Examples:
| Layout | Widths |
|---|---|
| 4 equal cards | 3 + 3 + 3 + 3 = 12 |
| 3 equal cards | 4 + 4 + 4 = 12 |
| 2 equal cards | 6 + 6 = 12 |
| 1 full-width card | 12 |
| 1 wide + 1 narrow | 8 + 4 = 12 |
| 1 wide + 2 narrow | 6 + 3 + 3 = 12 |
6.10 Nested dashboards require feature flag
Nested dashboards (embedding one dashboard inside another) are limited to 2 levels deep and are gated behind a feature flag. Not all projects have this enabled.
6.11 The markdown field accepts only HTML
Despite being named markdown, the field accepts only HTML tags. Markdown syntax (# Heading, **bold**, - list item) is not parsed and will render as literal text.
7. Dashboard Lifecycle Methods
7.1 Duplicate a dashboard
duplicate = ws.create_dashboard(CreateDashboardParams(
title="Product Health (Copy)",
duplicate=existing_dashboard_id,
))This creates a complete copy including all reports, text cards, and layout. The new dashboard has its own ID and independent content IDs.
Cross-project duplication is also possible via the raw API by including target_project_id alongside duplicate in the POST body.
7.2 Favorite / unfavorite
# Add to current user's favorites
ws.favorite_dashboard(dashboard_id)
# Remove from favorites
ws.unfavorite_dashboard(dashboard_id)Favorites are per-user. They appear in the "Favorites" filter in the Mixpanel dashboard list.
7.3 Pin / unpin
# Pin for all project members
ws.pin_dashboard(dashboard_id)
# Remove pin
ws.unpin_dashboard(dashboard_id)Pinned dashboards appear at the top of the dashboard list for all project members. Use pinning for team-wide dashboards that everyone should see.
7.4 Full lifecycle example
import mixpanel_headless as mp
from mixpanel_headless.types import CreateDashboardParams, UpdateDashboardParams
import json
ws = mp.Workspace()
# 1. Create
dashboard = ws.create_dashboard(CreateDashboardParams(
title="Weekly Product Review",
description="Key metrics reviewed in the weekly product sync.",
time_filter={
"dateRange": {"type": "in the last", "window": {"unit": "day", "value": 7}},
"displayText": "Last 7 days"
},
))
# 2. Add content
ws.update_dashboard(dashboard.id, UpdateDashboardParams(
content={"action": "create", "content_type": "text",
"content_params": {"markdown": "<h2>Weekly Product Review</h2><p>Updated every Monday.</p>"}}))
result = ws.query("Login", from_date="2025-01-01", to_date="2025-03-31")
ws.update_dashboard(dashboard.id, UpdateDashboardParams(
content={"action": "create", "content_type": "report",
"content_params": {"bookmark": {"name": "DAU Trend", "type": "insights",
"params": json.dumps(result.params)}}}))
# 3. Arrange layout (read IDs first)
dash = ws.get_dashboard(dashboard.id)
# ... build layout patch from dash.layout ...
# 4. Pin for the team
ws.pin_dashboard(dashboard.id)
# 5. Later: update title
ws.update_dashboard(dashboard.id, UpdateDashboardParams(
title="Weekly Product Review (Q2 2025)"
))
# 6. Cleanup: delete when no longer needed
ws.delete_dashboard(dashboard.id)8. Update Operations
8.1 Operation Ordering
When making multiple changes to an existing dashboard, operations must execute in this order:
Metadata (title, description) — standalone PATCH
Cell creates — add new content first
Row reorder (
rows_order) — after creates, so temp IDs can be resolvedCell updates — modify existing content in place
Cell deletes — remove content
Row deletes — remove entire rows last
Wrong order causes failures: reordering before creating temp rows gives "unknown row ID"; deleting before creating can leave layout gaps.
Re-fetch the dashboard (ws.get_dashboard()) between layout-modifying operations to get updated row/cell IDs.
8.2 Combined Content + Layout PATCH
To add a cell to a specific existing row (not just append to the bottom), send both content and layout in the same UpdateDashboardParams:
import copy
dash = ws.get_dashboard(dashboard_id)
layout = copy.deepcopy(dash.layout)
target_row = layout["rows"][target_row_id]
# Redistribute widths: 12 / (N+1) for all cells including new
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"],
},
))Without the layout field, new content always appends as a full-width row at the bottom.
8.3 Temp ID Resolution for New Rows
When adding content to a new row during an update:
Use a temp string ID for the new row (e.g.,
"temp-row-1")Execute the cell create — the new row appears in the dashboard response with a real ID
Diff row sets before/after to discover the real row ID
Use the real ID in subsequent
rows_orderoperations
before_rows = set(dash.layout["rows"].keys())
ws.update_dashboard(dashboard_id, UpdateDashboardParams(
content={"action": "create", "content_type": "text",
"content_params": {"markdown": "<h2>New Section</h2>"}},
))
dash = ws.get_dashboard(dashboard_id)
after_rows = set(dash.layout["rows"].keys())
new_row_id = (after_rows - before_rows).pop()8.4 Cross-Type Cell Updates
The API rejects changing content_type on an update action (e.g., converting a text cell to a report cell). To achieve this:
Delete the existing cell
Create a new cell with the desired type
# Delete old text card
ws.update_dashboard(dashboard_id, UpdateDashboardParams(
content={"action": "delete", "content_type": "text", "content_id": old_text_id},
))
# Create new report in its place
ws.update_dashboard(dashboard_id, UpdateDashboardParams(
content={"action": "create", "content_type": "report",
"content_params": {"bookmark": {...}}},
))8.5 Report-Link vs Report
Dashboard cells have two report content types:
content_type | Ownership | Editable? |
|---|---|---|
"report" | Owned by this dashboard | Yes — can update params, name, description |
"report-link" | Linked from another dashboard | No — read-only reference |
When analyzing or modifying existing dashboards, check the content_type in layout cells. Attempting to update a report-link's params will fail. Use update_report_link() only to change the link type, not the underlying report.
9. Dashboard Analysis
9.1 Extracting Dashboard Structure
get_dashboard() returns a Dashboard with layout and contents fields. Parse them to understand what a dashboard contains:
import json, re
dash = ws.get_dashboard(dashboard_id)
layout = dash.layout # {"version": "2.0.0", "order": [...], "rows": {...}}
contents = dash.contents # {"report": {...}, "text": {...}}
rows = []
for row_id in layout["order"]:
row = layout["rows"][row_id]
cells = []
for cell in row["cells"]:
cid = str(cell["content_id"])
ctype = cell["content_type"]
if ctype in ("report", "report-link"):
info = contents["report"][cid]
params = json.loads(info["params"]) if isinstance(info["params"], str) else (info.get("params") or {})
cells.append({
"type": ctype,
"name": info.get("name", ""),
"report_type": info.get("type", "insights"),
"bookmark_id": info["id"],
"description": info.get("description", ""),
"width": cell["width"],
"is_owned": ctype == "report",
"params": params,
})
elif ctype == "text":
md = contents["text"].get(cid, {}).get("markdown", "")
cells.append({
"type": "text",
"markdown": md,
"is_section_header": bool(re.search(r'<h2[\s>]', md, re.I)),
"width": cell["width"],
})
rows.append({"row_id": row_id, "height": row.get("height", 0), "cells": cells})9.2 Section Header Detection
Text cards with <h2> tags act as section dividers. Detect them with:
is_section_header = bool(re.search(r'<h2[\s>]', markdown, re.I))This lets you group reports by section, which is essential for:
Understanding dashboard organization
Responding to "add a report to the Retention section"
Generating section-aware summaries
9.3 Executing Dashboard Reports
Execute each report to get live data as a DataFrame:
for row in rows:
for cell in row["cells"]:
if cell["type"] not in ("report", "report-link"):
continue
bid = cell["bookmark_id"]
btype = cell["report_type"]
if btype == "flows":
result = ws.query_saved_flows(bid)
else:
result = ws.query_saved_report(bid, bookmark_type=btype)
df = result.df
# Summarize based on type:
# insights: df.describe(), trend = df.iloc[-1] vs df.iloc[-8]
# funnels: step conversion rates from result.series
# retention: day-N rates from result.series9.4 Cross-Dashboard Analysis
When working with multiple dashboards, build a unified data model:
import pandas as pd
dashboard_ids = [1001, 1002, 1003]
all_reports = {}
for did in dashboard_ids:
dash = ws.get_dashboard(did)
for cid, info in dash.contents.get("report", {}).items():
bid, btype = info["id"], info["type"]
try:
if btype == "flows":
result = ws.query_saved_flows(bid)
else:
result = ws.query_saved_report(bid, bookmark_type=btype)
all_reports[f"{dash.title} / {info['name']}"] = result.df
except Exception as e:
print(f"Skipping {info['name']}: {e}")
# Join on date index for cross-metric correlation
# combined = pd.concat([df1, df2], axis=1)
# correlation = combined.corr()Patterns to look for across dashboards:
Correlated metrics — DAU trend aligns with feature adoption
Divergent signals — signups up but retention down
Complementary data — one dashboard's KPIs explained by another's funnels
Gaps — metrics referenced in text cards but not visualized
Requirements and compatibility
Written for: OpenAI Codex
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Needs: Declares allowed tools, Needs Python
Security
No issues found by our static scan· No issues found
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- Created by
- Mixpanel
- Publisher
- OpenAI (Official)
- Distributed by
- OpenAI
- Source
- openai/plugins · plugins/mixpanel-headless/skills/dashboard-expert · commit d420d13
- Licence
- MIT License (redistribution allowed)
- LICENSE (added by AutoGineer from plugins/mixpanel-headless/LICENSE)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
AutoGineer is not affiliated with or endorsed by Mixpanel or OpenAI.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download dashboard-expert.zip