Generated banner for Dashboard Expert
Generated logo for Dashboard Expert

Dashboard Expert

OpenAIOfficialCreated by MixpanelDistributed by OpenAI

  • Skill
  • Tool integration
  • Data and AI
  • MIT
  • No issues found

From the author

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).
— the description by Mixpanel

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

  1. Download the skill as a ZIP:

    https://mp-staging.autogineer.com/agent-skills/openai/dashboard-expert/download
  2. Unzip 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 intentModeKey actions
"analyze/understand/read/explore dashboard"AnalyzeRead structure, execute reports, summarize
"build/create/make a new dashboard"BuildInvestigate data → plan → create with layout
"modify/update/add to/fix/improve dashboard"ModifyRead current state → plan changes → execute
"explain/annotate/add insights to dashboard"ExplainAnalyze → 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 team

Mode: 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.contents

Parse the response into a structured representation:

  • layout["order"] — ordered list of row IDs

  • layout["rows"][row_id]["cells"] — cells with content_id, content_type, width

  • contents["report"][str(content_id)] — report metadata: id (bookmark_id), name, type, params, description

  • contents["text"][str(content_id)] — text card: markdown

Classify each cell:

  • content_type == "report" → owned, editable

  • content_type == "report-link" → linked from another dashboard, read-only

  • content_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.

Phase A2: Extract Report Details

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 type

  • params["sections"]["group"] — breakdown properties

  • params["sections"]["filter"] — active filters

  • params["sections"]["time"] — date range

  • params["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.df

Summarize by report type:

TypeKey metrics to extract
insightsTotal, average, latest value, min, max, trend direction
funnelsStep names, counts, per-step and overall conversion rate
retentionDay 1, Day 7, Day 30 rates; stabilization point
flowsTop 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:

  1. Dashboard overview — title, purpose, section count, report count

  2. Section-by-section breakdown — what each section measures, key findings

  3. Cross-metric insights — correlations, anomalies, patterns

  4. 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 correlations

Mode: 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 default

  • Favorite for personal use: ws.favorite_dashboard(dashboard.id)

  • Add explainer cards: see Mode: Explain

  • Adjust heights: see references/dashboard-reference.md Section 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:

  1. Metadata (title/description) — standalone PATCH

  2. Cell creates — add new content first

  3. Row reorder (rows_order) — after creates so temp IDs resolve

  4. Cell updates — modify existing content

  5. Cell deletes — remove content

  6. 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.

  1. Analyze — run Mode: Analyze to extract structure and execute reports

  2. 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", "")
  3. 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

  1. 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.

  2. Width auto-redistribution — when adding to an existing row with N cells, set all cells (including new) to 12 // (N+1) width.

  3. Update operation ordering — metadata → cell creates → rows_order → cell updates → cell deletes → row deletes. Wrong order causes failures.

  4. per_user requires math_property — using per-user aggregation without a numeric property raises BookmarkValidationError.

  5. CreateBookmarkParams(dashboard_id=X) does NOT add to layout — use add_report_to_dashboard() or inline content action.

  6. add_report_to_dashboard() CLONES — creates "Duplicate of..." copy. Use rows in CreateDashboardParams or inline content action instead.

  7. GET order vs PATCH rows_order — layout from GET uses order; PATCH expects rows_order.

  8. Never include version in layout PATCH — the API rejects it.

  9. Strip \n and collapse whitespace — call .replace("\n", "").strip() on text card HTML. Newlines cause TipTap to mangle content.

  10. Limits — title 255 chars, description 400 chars, text cards 2,000 chars, max 4 items/row, max 30 rows.

  11. Cross-type cell updates require delete+create — API rejects changing content_type on an update action.

  12. 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.

  13. Auto-pin after creation — dashboards are invisible to the team by default. Call ws.pin_dashboard(dashboard.id).

  14. The markdown field 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 patterns

  • references/dashboard-templates.md — 9 purpose-built dashboard templates with section layouts and report specs

  • references/bookmark-pipeline.md — End-to-end pipeline from typed query to dashboard report for all 4 engines

  • references/chart-types.md — Chart type selection guide with slugs, use cases, and width recommendations

Files

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

FieldTypeDefaultConstraints
titlestrrequiredMax 255 chars
descriptionstr | NoneNoneMax 400 chars
is_privatebool | NoneNone (False)
is_restrictedbool | NoneNone (False)
filterslist[Any] | NoneNoneDashboard-level filters
breakdownslist[Any] | NoneNoneDashboard-level breakdowns
time_filterAny | NoneNoneDashboard-level time filter (see Section 5)
duplicateint | NoneNoneID 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.

FieldTypeDefaultConstraints
titlestr | NoneNoneMax 255 chars
descriptionstr | NoneNoneMax 400 chars
is_privatebool | NoneNone
is_restrictedbool | NoneNone
filterslist[Any] | NoneNoneDashboard-level filters
breakdownslist[Any] | NoneNoneDashboard-level breakdowns
time_filterAny | NoneNoneDashboard-level time filter
layoutAny | NoneNoneLayout patch (see Section 3)
contentAny | NoneNoneContent 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

FieldTypeDefaultNotes
markdownstr | NoneNoneHTML 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

FieldTypeDefaultNotes
link_typestrrequiredSerialized 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:

FieldTypeDescription
idintUnique dashboard identifier
titlestrDashboard title
descriptionstr | NoneDashboard description
is_privateboolWhether dashboard is private
is_restrictedboolWhether access is restricted
creator_idint | NoneCreator's user ID
creator_namestr | NoneCreator's name
creator_emailstr | NoneCreator's email
createddatetime | str | NoneCreation timestamp
modifieddatetime | str | NoneLast modification timestamp
is_favoritedbool | NoneWhether current user favorited it
pinned_datestr | NoneDate pinned, if any
layout_versionstr | NoneLayout version metadata
unique_view_countint | NoneNumber of unique viewers
total_view_countint | NoneTotal 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) -> Dashboard

Creates 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 operations
Get dashboard
def get_dashboard(self, dashboard_id: int) -> Dashboard

Retrieves 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) -> Dashboard

Updates an existing dashboard. Only provided fields are modified. Returns the updated Dashboard.

Used for three distinct operations:

  1. Metadata updates -- title, description, privacy, time filter

  2. Content actions -- add/remove/update reports and text cards (via content field)

  3. Layout patches -- rearrange rows, resize cells (via layout field)

# 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) -> None

Permanently deletes a dashboard. No return value.

Bulk delete dashboards
def bulk_delete_dashboards(self, ids: list[int]) -> None

Deletes 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) -> None

Toggles 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) -> None

Pinned 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) -> Dashboard

CLONES 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) -> Dashboard

Removes 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) -> None

Updates 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>"
))
def update_report_link(self, dashboard_id: int, report_link_id: int, params: UpdateReportLinkParams) -> None

Updates 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
TypeDescription
reportA report card (insights, funnels, retention, flows)
textA text/markdown card
Actions
ActionDescription
createAdd new content to the dashboard
deleteRemove content from the dashboard
updateModify existing content in place
moveMove content to a different position (prefer layout PATCH — Section 3 — instead)
duplicateDuplicate content within the dashboard
undeleteRestore 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:

FieldDescription
versionAlways "2.0.0". Never include in PATCH.
orderOrdered list of row IDs. Called order in GET response.
rowsDict mapping row ID to {height, cells}.
rows[].heightRow height in pixels. 0 means auto (text-only rows).
rows[].cellsOrdered list of cells in the row.
cells[].idCell identifier (UUID string).
cells[].widthCell width (1-12). Sum of all cells in a row must equal 12.
cells[].content_idNumeric ID of the content item.
cells[].content_typeOne 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 responsePATCH 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
ContentWidthRationale
Text card (section header)12Always full width
KPI metric card3 or 4Pack 3-4 per row
Line/bar chart (paired)6Side-by-side comparison
Line/bar chart (solo)12Full width for detail
Table12Needs full width for columns
Funnel (3+ steps)12Complex funnels need space
Retention curve12Full width for cohort grid
Sankey/flow12Always full width
3.6 Height Guidelines
Row ConfigurationHeight (px)Notes
Text-only row0Auto height, expands to fit content
KPI row (3-4 metric cards)336Compact number display
Two charts side-by-side (6+6)418Standard chart height
Single full-width chart500More vertical space for detail
Full-width funnel or table588Extra 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
CategoryTagsNotes
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:

TagReasonUse Instead
<div>Stripped by sanitizer<p>
<span>Stripped by sanitizerInline formatting tags (<strong>, <em>, etc.)
<b>Non-semantic<strong>
<i>Non-semantic<em>
<img>Not supported—
<table>, <tr>, <td>, <th>Not supportedUse a report card with table chart type
Any inline style attributesStrippedUse semantic tags
Any class attributesStrippedUse semantic tags
4.3 Formatting Rules
  1. Strip all \n newlines 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.

  2. 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 \n for line breaks.

  3. Character limit: Practical limit is 2,000 characters. Keep text cards under 500 characters for readability. Dashboard cards have limited vertical space.

  4. No Markdown syntax. Despite the field being called markdown, it accepts only HTML. Do not send # Heading or **bold** -- use <h2>Heading</h2> and <strong>bold</strong>.

  5. Section header pattern: <h2>Section Title</h2><p>One sentence description.</p> -- keep section titles to 2-4 words.

  6. 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 None

6. 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 Dashboard to 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
ConstraintLimit
Dashboard titleMax 255 characters
Dashboard descriptionMax 400 characters
Cell widths per rowMust sum to exactly 12
Items per rowMax 4
Rows per dashboardMax 30
Nested dashboard depthMax 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:

LayoutWidths
4 equal cards3 + 3 + 3 + 3 = 12
3 equal cards4 + 4 + 4 = 12
2 equal cards6 + 6 = 12
1 full-width card12
1 wide + 1 narrow8 + 4 = 12
1 wide + 2 narrow6 + 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:

  1. Metadata (title, description) — standalone PATCH

  2. Cell creates — add new content first

  3. Row reorder (rows_order) — after creates, so temp IDs can be resolved

  4. Cell updates — modify existing content in place

  5. Cell deletes — remove content

  6. 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:

  1. Use a temp string ID for the new row (e.g., "temp-row-1")

  2. Execute the cell create — the new row appears in the dashboard response with a real ID

  3. Diff row sets before/after to discover the real row ID

  4. Use the real ID in subsequent rows_order operations

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:

  1. Delete the existing cell

  2. 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": {...}}},
))

Dashboard cells have two report content types:

content_typeOwnershipEditable?
"report"Owned by this dashboardYes — can update params, name, description
"report-link"Linked from another dashboardNo — 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.series
9.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