# mixpanel_headless > Python library for working with Mixpanel analytics data, designed for AI coding agents mixpanel_headless is a complete programmable interface to Mixpanel analytics. Python library and CLI for discovery, querying, streaming, and entity management. Discover your schema, run live analytics (segmentation, funnels, retention, flows), execute JQL, and stream data for processing. # Getting Started # mixpanel_headless A complete programmable interface to Mixpanel analyticsβ€”available as both a Python library and CLI. Supports service account and OAuth 2.0 authentication. AI-Friendly Documentation πŸ€– **[Explore on DeepWiki β†’](https://deepwiki.com/mixpanel/mixpanel-headless)** DeepWiki provides an AI-optimized view of this projectβ€”perfect for code assistants, agents, and LLM-powered workflows. Ask questions about the codebase, explore architecture, or get contextual help. Google Code Wiki πŸ” **[Explore on Code Wiki β†’](https://codewiki.google/github.com/mixpanel/mixpanel-headless)** Google's Code Wiki offers another AI-optimized interface for exploring this codebaseβ€”search, understand, and navigate the project with natural language queries. ## Why This Exists Mixpanel's web UI is built for interactive exploration. But many workflows need something different: scripts that run unattended, notebooks that combine Mixpanel data with other sources, agents that query analytics programmatically, or pipelines that move data between systems. `mixpanel_headless` provides direct programmatic access to Mixpanel's analytics platform. Core analyticsβ€”typed insights queries, typed funnel queries, typed retention queries, typed flow queries, typed user profile queries, segmentation, saved reportsβ€”plus capabilities like raw JQL execution and streaming data extraction are available as Python methods or shell commands. ## Two Interfaces, One Capability Set **Python Library** β€” For notebooks, scripts, and applications: ``` import mixpanel_headless as mp ws = mp.Workspace() # Discover what's in your project events = ws.events() props = ws.properties("Purchase") values = ws.property_values("country", event="Purchase") funnels = ws.funnels() cohorts = ws.cohorts() bookmarks = ws.list_bookmarks() # Manage entities dashboards = ws.list_dashboards() cohort = ws.create_cohort(mp.CreateCohortParams(name="Power Users")) flags = ws.list_feature_flags() experiments = ws.list_experiments() # Operational tooling alerts = ws.list_alerts() annotations = ws.list_annotations(from_date="2025-01-01") webhooks = ws.list_webhooks() # Data governance event_defs = ws.get_event_definitions(names=["Signup"]) drop_filters = ws.list_drop_filters() custom_props = ws.list_custom_properties() lookup_tables = ws.list_lookup_tables() # Schema governance schemas = ws.list_schema_registry() enforcement = ws.get_schema_enforcement() audit = ws.run_audit() # Insights queries β€” typed, composable analytics from mixpanel_headless import Metric, Filter, Formula # Simple event query (last 30 days by default) result = ws.query("Login") print(result.df) # DAU with breakdown result = ws.query("Login", math="dau", group_by="platform", last=90) # Multi-metric formula: conversion rate result = ws.query( [Metric("Signup", math="unique"), Metric("Purchase", math="unique")], formula="(B / A) * 100", formula_label="Conversion Rate", unit="week", ) # Filtered aggregation with numeric breakdown result = ws.query( "Purchase", math="total", math_property="amount", where=[Filter.equals("country", "US"), Filter.greater_than("amount", 50)], group_by="platform", ) # Typed funnel query β€” define steps inline funnel_result = ws.query_funnel( ["Signup", "Add to Cart", "Purchase"], conversion_window=7, last=90, ) print(funnel_result.overall_conversion_rate) # Typed retention query β€” cohort retention with event pairs from mixpanel_headless import RetentionEvent retention_result = ws.query_retention( "Signup", "Login", retention_unit="week", last=90, ) print(retention_result.df.head()) # cohort_date | bucket | count | rate # Typed flow query β€” analyze user paths through your product from mixpanel_headless import FlowStep flow_result = ws.query_flow("Purchase", forward=3, reverse=1) print(flow_result.nodes_df.head()) # step | event | type | count print(flow_result.top_transitions(5)) # Typed user profile query β€” search and aggregate user profiles from mixpanel_headless import Filter result = ws.query_user( where=Filter.equals("plan", "premium"), properties=["$email", "$name", "ltv"], sort_by="ltv", sort_order="descending", limit=50, ) print(f"{result.total} premium users") print(result.df) # Cohort-scoped queries β€” filter, break down, or track cohorts from mixpanel_headless import CohortCriteria, CohortDefinition, CohortBreakdown, CohortMetric # Define a cohort inline and use it immediately power_users = CohortDefinition( CohortCriteria.did_event("Purchase", at_least=3, within_days=30) ) result = ws.query("Login", where=Filter.in_cohort(power_users, name="Power Users")) result = ws.query("Login", group_by=CohortBreakdown(power_users, name="Power Users")) result = ws.query(CohortMetric(123, "Power Users"), last=90, unit="week") # Legacy live queries segmentation = ws.segmentation( event=events[0], from_date="2025-01-01", to_date="2025-01-31", on="country" ) funnel = ws.funnel( funnel_id=funnels[0].id, from_date="2025-01-01", to_date="2025-01-31" ) # Stream events for processing for event in ws.stream_events(from_date="2025-01-01", to_date="2025-01-31"): process(event) # Results have .df for pandas interoperability result.df segmentation.df funnel.df ``` **CLI** β€” For shell scripts, pipelines, and agent tool calls: ``` # Discover your data landscape mp inspect events mp inspect properties --event Purchase mp inspect values --event Purchase --property country mp inspect top-events mp inspect funnels mp inspect cohorts mp inspect bookmarks # Manage entities mp dashboards list mp reports list --type insights mp cohorts create --name "Power Users" mp flags list mp experiments list mp alerts list mp annotations list --from-date 2025-01-01 mp webhooks list # Data governance mp lexicon events get --names Signup,Login mp drop-filters list mp custom-properties list mp lookup-tables list # Schema governance mp schemas list mp lexicon enforcement get mp lexicon audit # Live queries against Mixpanel API mp query segmentation "Purchase" \ --from 2025-01-01 --to 2025-01-31 --on country mp query funnel 12345 --from 2025-01-01 --to 2025-01-31 mp query retention \ --born-event Signup --return-event Purchase --from 2025-01-01 mp query activity-feed user@example.com --from 2025-01-01 mp query saved-report 67890 mp query frequency "Login" --from 2025-01-01 # Filter with built-in jq mp query segmentation "Purchase" --from 2025-01-01 --format json --jq '.total' # Stream events via Python API (memory-efficient for large datasets) # for event in ws.stream_events(from_date="2025-01-01", to_date="2025-01-31"): # process(event) ``` ## Capabilities **Discovery** β€” Rapidly explore your project's data landscape: - List all events, drill into properties, sample actual values - Browse saved funnels, cohorts, and reports (bookmarks) - Access Lexicon definitions from your data dictionary - Analyze property distributions, coverage, and numeric statistics - Inspect top events by volume, daily trends, user engagement patterns Discovery commands let you survey what exists before writing queriesβ€”no guessing at event names or property values. **Insights Queries** β€” Typed, composable analytics using Mixpanel's Insights engine: - DAU / WAU / MAU and unique user metrics - Multi-metric comparison on a single chart - Formula-based metrics (conversion rates, ratios) - Per-user aggregation (average purchases per user) - Rolling and cumulative analysis modes - Percentiles (p25, p75, p90, p99, custom percentiles, histogram distributions) - Typed filters (`Filter.equals()`, `Filter.greater_than()`, date filters like `Filter.in_the_last()`, etc.) - Cohort-scoped queries β€” filter by cohort, break down by cohort membership, or track cohort size over time, using saved cohort IDs or inline `CohortDefinition` objects - Property breakdowns with numeric bucketing - Results as DataFrames, persistable as saved reports **Live Queries** β€” Execute Mixpanel analytics directly: - Segmentation with filtering, grouping, and time bucketing - Typed funnel queries with ad-hoc step definitions, exclusions, and conversion windows - Typed retention queries with event pairs, custom buckets, alignment modes, and segmentation - Typed flow queries with path analysis, direction controls, and visualization modes - Typed user profile queries with filtering, sorting, property selection, and aggregation - Funnel conversion analysis (legacy saved funnels) - Retention analysis (legacy) - Saved reports (Insights, Funnels, Flows, Retention) - User activity feeds - Frequency and engagement analysis - Numeric aggregations (sum, average, bucket) - Raw JQL execution for custom analysis **Entity Management** β€” Create, update, and delete Mixpanel entities: - Full CRUD for dashboards, reports (bookmarks), cohorts, feature flags, experiments, alerts, annotations, and webhooks - Bulk operations for efficient batch management - Dashboard features: favorites, pins, blueprint templates, RCA dashboards - Report history tracking and linked dashboard discovery - Feature flag lifecycle (enable/disable/archive) with test users and history - Experiment lifecycle management (draft/launch/conclude/decide) - Alert monitoring: trigger history, test notifications, screenshot URLs, bookmark validation - Timeline annotations with tagging system - Webhook management with connectivity testing **Data Governance** β€” Define and control your data taxonomy: - Lexicon definitions: manage event and property metadata, tags, descriptions, visibility - Drop filters: suppress unwanted events at ingestion - Custom properties: create computed properties from formulas or behaviors - Custom events: manage composite event definitions - Lookup tables: upload, download, and manage CSV reference data for property enrichment - Tracking metadata, change history, and bulk export for audit and governance workflows **Streaming** β€” Process data without storage: - Stream events directly for ETL pipelines - One-time processing without local persistence - Memory-efficient iteration over large datasets ## For Humans and Agents The structured output and deterministic command interface make `mixpanel_headless` particularly effective for AI coding agentsβ€”the same properties that make it scriptable for humans make it reliable for automated workflows. Discovery commands are particularly valuable: an agent can rapidly survey your data landscapeβ€”listing events, inspecting properties, sampling valuesβ€”then construct accurate queries based on what actually exists rather than guessing. The tool is designed to be self-documenting: comprehensive `--help` on every command, complete docstrings on every method, full type annotations throughout, and rich exception messages that explain what went wrong and how to fix it. Agents can discover capabilities, learn correct usage, and recover from mistakes autonomously. ### LLM-Optimized Documentation This documentation is built with AI consumption in mind. In addition to the standard HTML pages, we provide: | Endpoint | Size | Use Case | | ----------------------------------------------------------------------------- | ------ | -------------------------------------------------------------- | | [`llms.txt`](https://mixpanel.github.io/mixpanel-headless/llms.txt) | ~3KB | Structured indexβ€”discover what documentation exists | | [`llms-full.txt`](https://mixpanel.github.io/mixpanel-headless/llms-full.txt) | ~400KB | Complete documentation in one fileβ€”comprehensive search | | [`index.md`](https://mixpanel.github.io/mixpanel-headless/index.md) pages | Varies | Each HTML page has a corresponding `index.md` at the same path | Every page also has a **Copy Markdown** button in the upper right cornerβ€”click it to copy the page content as markdown, ready to paste into your AI assistant's context. For interactive exploration of the codebase itself, see [DeepWiki](https://deepwiki.com/mixpanel/mixpanel-headless). ## Next Steps - [Installation](https://mixpanel.github.io/mixpanel-headless/getting-started/installation/index.md) β€” Get started with pip or uv - [Quick Start](https://mixpanel.github.io/mixpanel-headless/getting-started/quickstart/index.md) β€” Your first queries in 5 minutes - [Insights Queries](https://mixpanel.github.io/mixpanel-headless/guide/query/index.md) β€” Typed analytics queries with DAU, formulas, filters, and breakdowns - [Funnel Queries](https://mixpanel.github.io/mixpanel-headless/guide/query-funnels/index.md) β€” Typed funnel conversion analysis with steps, exclusions, and conversion windows - [Retention Queries](https://mixpanel.github.io/mixpanel-headless/guide/query-retention/index.md) β€” Typed retention analysis with event pairs, custom buckets, and alignment modes - [Flow Queries](https://mixpanel.github.io/mixpanel-headless/guide/query-flows/index.md) β€” Typed flow path analysis with direction controls and visualization modes - [User Profile Queries](https://mixpanel.github.io/mixpanel-headless/guide/query-users/index.md) β€” Typed user profile queries with filtering, sorting, and aggregation - [API Reference](https://mixpanel.github.io/mixpanel-headless/api/index.md) β€” Complete Python API documentation - [Entity Management](https://mixpanel.github.io/mixpanel-headless/guide/entity-management/index.md) β€” Manage dashboards, reports, cohorts, feature flags, experiments, alerts, annotations, and webhooks - [Data Governance](https://mixpanel.github.io/mixpanel-headless/guide/data-governance/index.md) β€” Manage Lexicon definitions, drop filters, custom properties, custom events, and lookup tables - [Business Context](https://mixpanel.github.io/mixpanel-headless/guide/business-context/index.md) β€” Read and write the markdown business context that grounds AI assistants (org and project scopes) - [CLI Reference](https://mixpanel.github.io/mixpanel-headless/cli/index.md) β€” Command-line interface documentation Copy markdown # Installation > **⚠️ Pre-release Software**: This package is under active development. APIs may change between versions before 1.0. Explore on DeepWiki πŸ€– **[Installation Guide β†’](https://deepwiki.com/mixpanel/mixpanel-headless/2.1-installation)** Ask questions about requirements, dependencies, or troubleshoot installation issues. ## Requirements - Python 3.10 or higher - A Mixpanel service account with API access ## Installing with pip ``` pip install mixpanel-headless ``` ## Installing with uv [uv](https://github.com/astral-sh/uv) is a fast Python package installer: ``` uv pip install mixpanel-headless ``` Or add to your project: ``` uv add mixpanel-headless ``` ## Optional Dependencies ### Documentation Tools If you want to build the documentation locally: ``` pip install mixpanel_headless[docs] ``` ## Verifying Installation After installation, verify the CLI is available: ``` mp --version ``` You should see the installed version printed. Test the Python import: ``` import mixpanel_headless as mp print(mp.__version__) ``` ## Next Steps - [Quick Start](https://mixpanel.github.io/mixpanel-headless/getting-started/quickstart/index.md) β€” Set up credentials and run your first query - [Configuration](https://mixpanel.github.io/mixpanel-headless/getting-started/configuration/index.md) β€” Learn about environment variables and config files Copy markdown # Quick Start This guide walks you through your first queries with mixpanel_headless in about 5 minutes. Explore on DeepWiki πŸ€– **[Quick Start Tutorial β†’](https://deepwiki.com/mixpanel/mixpanel-headless/2.3-quick-start-tutorial)** Ask questions about getting started, explore example workflows, or troubleshoot common issues. ## Prerequisites You'll need: - mixpanel_headless installed (`pip install mixpanel-headless`) - **Either** a Mixpanel service account (username, secret, project ID) **or** a Mixpanel user account (for OAuth login) - Your project's data residency region (us, eu, or in) ## Step 1: Set Up Credentials ### Recommended: `mp login` ``` mp login # Opens browser for PKCE... # Authenticated as jared@example.com # # Found 2 project(s) across 1 organization(s): # 1) Acme Β· AI Demo (id 3713224, mixpanel.com) # 2) Acme Β· E-Commerce (id 3018488, mixpanel.com) # Which project? [1]: 1 # # Logged in as jared@example.com β†’ acme Β· AI Demo ``` `mp login` is the one-shot path β€” it picks the right auth flow for your environment, hits `/me` to discover what you can access, derives the account name from your org, and pins a default project (auto-picks when you have one, prompts when you have several). For browser PKCE, tokens land at `~/.mp/accounts/{name}/tokens.json` (mode `0o600`) and refresh automatically on expiry. The command auto-detects your auth type from the environment: | Env vars set | Auth type used | Region behavior | Persistence | | --------------------------- | ---------------------- | ------------------------------------------------------------ | --------------------------------------------------------------------------------------------------- | | `MP_USERNAME` + `MP_SECRET` | `service_account` | probes `us β†’ eu β†’ in` | username + secret persisted to `~/.mp/config.toml` | | `MP_OAUTH_TOKEN` | `oauth_token` | probes `us β†’ eu β†’ in` | bearer persisted inline to `~/.mp/config.toml` (use `--token-env VAR` to persist a pointer instead) | | Neither | `oauth_browser` (PKCE) | defaults to `us` (pass `--region eu\|in` for other clusters) | refresh-capable tokens at `~/.mp/accounts/{name}/tokens.json` | For a truly non-persistent path (env-only with no account record), set the env vars and skip `mp login` entirely β€” the resolver picks them up directly. See the "Other auth paths" tabs below. Useful flags: `--region {us\|eu\|in}` sets the region explicitly (skips the probe for SA / token paths), `--project ID` skips the picker, `--name NAME` overrides the derived account name, `--service-account` / `--token-env VAR` force a non-browser path. ### Other auth paths For unattended automation, set the four env vars and skip account registration entirely β€” the resolver picks them up directly: ``` export MP_USERNAME="sa_abc123..." export MP_SECRET="your-secret-here" export MP_PROJECT_ID="12345" export MP_REGION="us" ``` If a managed OAuth client hands you a pre-obtained access token, inject it via env vars (the library sends `Authorization: Bearer `): ``` export MP_OAUTH_TOKEN="" export MP_PROJECT_ID="12345" export MP_REGION="us" # or "eu", "in" ``` Tokens injected this way are not persisted (no refresh β€” pass a fresh token when the previous one expires). The full service-account env-var set takes precedence when both sets are complete. For full control over the account name and region at registration time: ``` mp account add personal --type oauth_browser --region us mp account login personal # βœ“ Authenticated as jared@example.com ``` `mp login --name personal --region us` is the one-line equivalent. See [Configuration β†’ OAuth (browser) β€” token storage](https://mixpanel.github.io/mixpanel-headless/getting-started/configuration/#oauth-browser-token-storage) for the persistence details. ## Step 2: Switch Projects (Optional) `mp login` already pinned the project shown in its success line. This step is for *changing* it later β€” pointing the same account at a different project, or swapping in-session. ``` mp project list # ID NAME ORG WORKSPACES # 3713224 AI Demo Acme βœ“ # 3018488 E-Commerce Demo Acme βœ“ mp project use 3018488 # Active project: E-Commerce Demo (3018488) ``` ``` import mixpanel_headless as mp ws = mp.Workspace() for project in ws.projects(): print(project.id, project.name) ws.use(project="3018488", persist=True) ``` `mp project use` writes to the active account's `default_project`. To override per-call without persisting, pass `--project` / `-p` on the CLI or `Workspace(project="...")` in Python. Env-only paths skip this step `mp project use` requires an active account in `~/.mp/config.toml`. If you set up via the service-account env quad or `MP_OAUTH_TOKEN` without registering an account, set the project via `MP_PROJECT_ID` directly (already required by both env-only paths) or pass `--project` / `Workspace(project=...)` per call. Don't run `mp project use` β€” it errors with "No active account configured." ## Step 3: Test Your Connection Verify credentials are working: ``` mp account test # { "account_name": "production", "ok": true, "user": {...}, "accessible_project_count": 7 } ``` ``` import mixpanel_headless as mp result = mp.accounts.test() # AccountTestResult; never raises β€” check result.ok / result.error if result.ok: print(result.user.email, result.accessible_project_count) else: print("test failed:", result.error) ``` ## Step 4: Explore Your Data Before writing queries, survey your data landscape. Discovery commands let you see what exists in your Mixpanel project without guessing. ### List Events ``` mp inspect events ``` ``` import mixpanel_headless as mp ws = mp.Workspace() events = ws.events() # list[str] for name in events[:10]: print(name) ``` ### Drill Into Properties Once you know an event name, see what properties it has: ``` mp inspect properties --event Purchase ``` ``` props = ws.properties("Purchase") # list[str] for name in props: print(name) ``` ### Sample Property Values See actual values a property contains: ``` mp inspect values --event Purchase --property country ``` ``` values = ws.property_values("country", event="Purchase") print(values) # ['US', 'UK', 'DE', 'FR', ...] ``` ### See What's Active Check today's top events by volume: ``` mp inspect top-events ``` ``` top = ws.top_events() for e in top[:5]: print(f"{e.name}: {e.count:,} events") ``` ### Browse Saved Assets See funnels, cohorts, and saved reports already defined in Mixpanel: ``` mp inspect funnels mp inspect cohorts mp inspect bookmarks ``` ``` funnels = ws.funnels() cohorts = ws.cohorts() bookmarks = ws.list_bookmarks() ``` This discovery workflow ensures your queries reference real event names, valid properties, and actual valuesβ€”no trial and error. ## Step 5: Run Analytics Queries ### Insights Queries (Recommended) Use `query()` for typed, composable analytics β€” DAU/WAU/MAU, formulas, filters, breakdowns, and more: ``` import mixpanel_headless as mp from mixpanel_headless import Metric, Filter ws = mp.Workspace() # Simple event count (last 30 days by default) result = ws.query("Purchase") print(result.df) # DAU with property breakdown result = ws.query("Login", math="dau", group_by="platform", last=90) # Filtered aggregation result = ws.query( "Purchase", math="total", math_property="amount", where=Filter.equals("country", "US"), ) # Multi-metric formula result = ws.query( [Metric("Signup", math="unique"), Metric("Purchase", math="unique")], formula="(B / A) * 100", formula_label="Conversion Rate", ) ``` ### Cohort-Scoped Queries Scope any query to a user segment β€” define cohorts inline without saving them first: ``` from mixpanel_headless import CohortCriteria, CohortDefinition, Filter, CohortBreakdown # Define a cohort on the fly power_users = CohortDefinition( CohortCriteria.did_event("Purchase", at_least=3, within_days=30) ) # Filter to that cohort result = ws.query("Login", where=Filter.in_cohort(power_users, name="Power Users")) # Compare cohort vs. everyone else result = ws.query("Login", group_by=CohortBreakdown(power_users, name="Power Users")) ``` Cohort filters work across all five query methods. See the [Insights Queries guide β€” Cohort-Scoped Queries](https://mixpanel.github.io/mixpanel-headless/guide/query/#cohort-scoped-queries) for full coverage. ### Funnel Queries Define funnels inline with typed steps β€” no saved funnel required: ``` from mixpanel_headless import FunnelStep, Filter # Simple funnel result = ws.query_funnel(["Signup", "Purchase"]) print(f"Conversion: {result.overall_conversion_rate:.1%}") # With per-step filters and conversion window result = ws.query_funnel( [ FunnelStep("Signup"), FunnelStep("Purchase", filters=[Filter.greater_than("amount", 50)]), ], conversion_window=7, last=90, ) print(result.df) ``` See the [Funnel Queries guide](https://mixpanel.github.io/mixpanel-headless/guide/query-funnels/index.md) for full coverage. ### Retention Queries Measure cohort retention with typed event pairs β€” no saved report required: ``` from mixpanel_headless import RetentionEvent, Filter # Simple retention: do signups come back? result = ws.query_retention("Signup", "Login", retention_unit="week", last=90) print(result.df.head()) # cohort_date bucket count rate # 0 2025-01-01 0 1000 1.000000 # 1 2025-01-01 1 800 0.800000 # With per-event filters and custom buckets result = ws.query_retention( RetentionEvent("Signup", filters=[Filter.equals("source", "organic")]), "Login", retention_unit="day", bucket_sizes=[1, 3, 7, 14, 30], ) ``` See the [Retention Queries guide](https://mixpanel.github.io/mixpanel-headless/guide/query-retention/index.md) for full coverage. ### Flow Queries Analyze user paths through your product β€” what do users do before and after key events: ``` from mixpanel_headless import FlowStep, Filter # What happens after Purchase? result = ws.query_flow("Purchase", forward=3, last=90) print(result.top_transitions(5)) # With per-step filters and reverse analysis result = ws.query_flow( FlowStep("Purchase", filters=[Filter.greater_than("amount", 50)]), forward=3, reverse=2, ) print(result.nodes_df) print(result.edges_df) ``` See the [Flow Queries guide](https://mixpanel.github.io/mixpanel-headless/guide/query-flows/index.md) for full coverage. ### User Profile Queries Search, filter, and aggregate user profiles stored in Mixpanel: ``` from mixpanel_headless import Filter # Query user profiles result = ws.query_user( where=Filter.equals("plan", "premium"), properties=["$email", "$name", "ltv"], sort_by="ltv", sort_order="descending", limit=50, ) print(f"{result.total} premium users") print(result.df) # Count matching profiles count = ws.query_user(mode="aggregate", where=Filter.is_set("$email")) print(f"Users with email: {count.value}") ``` See the [User Profile Queries guide](https://mixpanel.github.io/mixpanel-headless/guide/query-users/index.md) for full coverage. ### Legacy Query Methods For segmentation, funnels, and retention via the older Query API: ``` mp query segmentation --event Purchase --from 2025-01-01 --to 2025-01-31 --format table # Filter results with built-in jq support mp query segmentation --event Purchase --from 2025-01-01 --to 2025-01-31 \ --format json --jq '.total' ``` ``` import mixpanel_headless as mp ws = mp.Workspace() result = ws.segmentation( event="Purchase", from_date="2025-01-01", to_date="2025-01-31" ) # Access as DataFrame print(result.df) ``` ## Step 6: Switch Accounts and Projects In-Session `Workspace.use()` swaps any axis without rebuilding the underlying HTTP client (O(1) per swap), so cross-project iteration is cheap: ``` import mixpanel_headless as mp ws = mp.Workspace() # In-session switching (returns self for chaining) ws.use(account="team") # implicitly clears workspace ws.use(project="3018488") ws.use(workspace=3448414) ws.use(target="ecom") # apply all three at once # Persist the new state ws.use(project="3018488", persist=True) # Iterate across projects for project in ws.projects(): ws.use(project=project.id) print(project.name, len(ws.events())) ``` See [Configuration β†’ Saved Targets](https://mixpanel.github.io/mixpanel-headless/getting-started/configuration/#saved-targets) for the full target workflow. ## Step 7: Manage Entities & Data Governance (Optional) Create, update, and delete dashboards, reports, cohorts, feature flags, and experiments: ``` # List your dashboards mp dashboards list # Create a cohort mp cohorts create --name "Premium Users" # List saved reports mp reports list --type insights # Feature flags and experiments mp flags list mp experiments create --name "Checkout Flow Test" # Data governance mp lexicon events get --names Signup mp drop-filters list mp custom-properties list mp lookup-tables list mp schemas list mp lexicon enforcement get mp lexicon audit ``` ``` import mixpanel_headless as mp ws = mp.Workspace() dashboards = ws.list_dashboards() cohort = ws.create_cohort(mp.CreateCohortParams(name="Premium Users")) reports = ws.list_bookmarks_v2(bookmark_type="insights") # Feature flags and experiments flags = ws.list_feature_flags() exp = ws.create_experiment(mp.CreateExperimentParams(name="Checkout Flow Test")) # Data governance event_defs = ws.get_event_definitions(names=["Signup"]) drop_filters = ws.list_drop_filters() schemas = ws.list_schema_registry() audit = ws.run_audit() ``` See the [Entity Management guide](https://mixpanel.github.io/mixpanel-headless/guide/entity-management/index.md) for complete coverage of dashboard, report, cohort, feature flag, and experiment operations. See the [Data Governance guide](https://mixpanel.github.io/mixpanel-headless/guide/data-governance/index.md) for Lexicon definitions, drop filters, custom properties, custom events, lookup tables, schema registry, schema enforcement, data auditing, and event deletion requests. ## Step 8: Stream Data For ETL pipelines or data processing, stream data directly: ``` import mixpanel_headless as mp ws = mp.Workspace() for event in ws.stream_events(from_date="2025-01-01", to_date="2025-01-31"): send_to_warehouse(event) ws.close() ``` ## Next Steps - [Configuration](https://mixpanel.github.io/mixpanel-headless/getting-started/configuration/index.md) β€” Multiple accounts and advanced settings - [Insights Queries](https://mixpanel.github.io/mixpanel-headless/guide/query/index.md) β€” Typed analytics with DAU, formulas, filters, and breakdowns - [Funnel Queries](https://mixpanel.github.io/mixpanel-headless/guide/query-funnels/index.md) β€” Typed funnel conversion analysis - [Retention Queries](https://mixpanel.github.io/mixpanel-headless/guide/query-retention/index.md) β€” Typed retention analysis with event pairs and custom buckets - [Flow Queries](https://mixpanel.github.io/mixpanel-headless/guide/query-flows/index.md) β€” Typed flow path analysis with direction controls and visualization modes - [User Profile Queries](https://mixpanel.github.io/mixpanel-headless/guide/query-users/index.md) β€” Typed user profile queries with filtering, sorting, and aggregation - [Live Analytics](https://mixpanel.github.io/mixpanel-headless/guide/live-analytics/index.md) β€” Segmentation, funnels, retention - [Entity Management](https://mixpanel.github.io/mixpanel-headless/guide/entity-management/index.md) β€” Manage dashboards, reports, cohorts, feature flags, and experiments - [Data Governance](https://mixpanel.github.io/mixpanel-headless/guide/data-governance/index.md) β€” Manage Lexicon definitions, drop filters, custom properties, and lookup tables - [Streaming Data](https://mixpanel.github.io/mixpanel-headless/guide/streaming/index.md) β€” Stream events and profiles for ETL pipelines Copy markdown # Configuration `mixpanel_headless` organizes auth around three independent axes: **Account β†’ Project β†’ Workspace**. - **Account** β€” *who* is authenticating. Three first-class types managed through one surface: `service_account` (Basic Auth), `oauth_browser` (PKCE flow with auto-refreshed tokens), and `oauth_token` (static bearer for CI / agents). - **Project** β€” *which* Mixpanel project the calls run against. Lives on the active account as `default_project`; can be overridden per call. - **Workspace** β€” *which* workspace inside the project. Optional; lazy-resolves to the project's default workspace on first workspace-scoped call. In-session switching is a one-line operation: `Workspace.use(account=..., project=..., workspace=..., target=...)`. The underlying HTTP client and per-account `/me` cache are preserved across switches, so cross-project / cross-account iteration is O(1) per turn. Explore on DeepWiki πŸ€– **[Authentication Setup β†’](https://deepwiki.com/mixpanel/mixpanel-headless/2.2-authentication-setup)** Ask questions about service accounts, OAuth, environment variables, or multi-account configuration. ## Quick Start: `mp login` The fastest way to authenticate is the top-level `mp login` command. It runs the right auth flow for your environment, derives an account name from `/me`, and pins a default project β€” all in one call: ``` mp login # Logged in as jared@example.com β†’ acme Β· AI Demo ``` Auth-type detection is env-driven: 1. Explicit `--service-account` / `--token-env VAR` flag. 1. `MP_USERNAME` + `MP_SECRET` set β†’ `service_account`. 1. `MP_OAUTH_TOKEN` set β†’ `oauth_token`. 1. Otherwise β†’ `oauth_browser` (PKCE). Region behavior depends on the auth type: - `service_account` and `oauth_token`: probes `us β†’ eu β†’ in` against `/me` and uses the first 200. - `oauth_browser`: defaults to `us` when `--region` is not passed. EU and India users must pass `--region eu` or `--region in` explicitly (the PKCE flow commits to a single region before the post-login `/me` probe runs). Useful flags: `--region {us|eu|in}` sets the region explicitly, `--project ID` skips the picker, `--name NAME` overrides the derived name. The rest of this page covers the underlying account model, env-var precedence, and the explicit setup paths for users who want more control than `mp login` provides. ## Account Types | Type | Best For | Storage | | ----------------- | ------------------------------------ | ---------------------------------------------------- | | `service_account` | CI / scripts / unattended automation | `~/.mp/config.toml` (Basic Auth username + secret) | | `oauth_browser` | Interactive personal use | `~/.mp/accounts/{name}/tokens.json` (auto-refreshed) | | `oauth_token` | CI bots / agents (no browser) | Inline secret OR `--token-env VAR` indirection | Service accounts are the right default for unattended automation. OAuth browser is the right default for personal/interactive use. OAuth token (static bearer) is the right default when a managed OAuth client (Claude Code plugin, CI pipeline) hands you a pre-obtained access token. ## Environment Variables | Variable | Purpose | | ---------------------- | -------------------------------------------------------------------------------------------------------------- | | `MP_ACCOUNT` | Active account override (CLI: `--account` / `-a`) | | `MP_PROJECT_ID` | Project override (CLI: `--project` / `-p`) | | `MP_WORKSPACE_ID` | Workspace override (CLI: `--workspace` / `-w`) | | `MP_TARGET` | Apply a saved target (CLI: `--target` / `-t`) | | `MP_OAUTH_TOKEN` | Static bearer token (alternative to a registered account; env-var path requires `MP_PROJECT_ID` + `MP_REGION`) | | `MP_USERNAME` | Service-account username (requires `MP_SECRET`, `MP_PROJECT_ID`, `MP_REGION`) | | `MP_SECRET` | Service-account secret (paired with `MP_USERNAME`) | | `MP_REGION` | Data residency region (`us`, `eu`, `in`) | | `MP_AUTH_FILE` | Override path to the Cowork bridge file | | `MP_CONFIG_PATH` | Override config file path (`~/.mp/config.toml`) | | `MP_OAUTH_STORAGE_DIR` | Override storage root (`~/.mp`) | These map onto the [credential resolution chain](#credential-resolution-chain) below. Service-account env quad takes precedence over `MP_OAUTH_TOKEN` If `MP_USERNAME` + `MP_SECRET` + `MP_PROJECT_ID` + `MP_REGION` are all set, the service-account quad wins even when `MP_OAUTH_TOKEN` is also present. This is safe to add to a shell that already exports the service-account vars. `mp login` auth-type detection The same env presence drives `mp login`'s auth-type detection: `MP_USERNAME` + `MP_SECRET` set β†’ `service_account`; `MP_OAUTH_TOKEN` set β†’ `oauth_token`; otherwise the browser PKCE flow. Pass `--service-account` or `--token-env VAR` to force a non-browser path. ## Setting Up an Account ### Service account (Basic Auth) ``` # Set the secret via env var (preferred) export MP_SECRET="aQUXhKokwLywLoxE3AxLt0g9dXC2G7bT" mp account add team --type service_account \ --username "team-mp.292e7c.mp-service-account" \ --project 3018488 \ --region us # Added account 'team' (service_account, us). Set as active. ``` Or read the secret from stdin (useful when it lives in a shell variable): ``` echo "$SECRET" | mp account add team --type service_account \ --username "team-mp..." --project 3018488 --region us --secret-stdin ``` Verify: ``` mp account test team # { "account_name": "team", "ok": true, "user": {...}, "accessible_project_count": 7 } ``` ### OAuth (browser, PKCE) The recommended path is `mp login` (see [Quick Start](#quick-start-mp-login) above), which derives a name from `/me`. The browser path defaults to `us`; pass `--region eu|in` for other clusters: ``` mp login --name personal # Logged in as jared@example.com β†’ personal Β· AI Demo ``` Tokens land at `~/.mp/accounts/personal/tokens.json` (mode `0o600`) and refresh automatically before each call. The `default_project` is set from the post-login `/me` probe. Advanced: explicit account creation (two-step) For full control over the account name and region at registration time: ``` mp account add personal --type oauth_browser --region us # Added account 'personal' (oauth_browser, us). Set as active. mp account login personal # Opening browser... # βœ“ Authenticated as jared@example.com ``` `mp account add` registers the account; `mp account login` runs the PKCE flow. `mp login --name personal --region us` collapses both into one call. ### OAuth (static bearer / CI) ``` # Pure env-var path β€” no persistent state export MP_OAUTH_TOKEN="ey..." export MP_REGION=us export MP_PROJECT_ID=3713224 mp query segmentation -e Login --from 2026-04-01 --to 2026-04-21 ``` Or register a named account that pulls the token from an env var at request time: ``` mp account add ci --type oauth_token --token-env MP_CI_TOKEN \ --project 3713224 --region us mp account use ci MP_CI_TOKEN=ey... mp query segmentation -e Login --from 2026-04-01 ``` `--project` is required when registering an `oauth_token` account (it becomes the account's `default_project`). Static bearers are not persisted (no refresh capability β€” pass a fresh token when the previous one expires). ## Config File Persistent state lives in `~/.mp/config.toml` (mode `0o600`), with a single schema and four section types: ``` [active] account = "personal" workspace = 3448414 # optional [accounts.personal] type = "oauth_browser" region = "us" default_project = "3713224" [accounts.team] type = "service_account" region = "us" default_project = "3018488" username = "team-mp..." secret = "..." [accounts.ci] type = "oauth_token" region = "us" default_project = "3713224" token_env = "MP_CI_TOKEN" # XOR with `token = "..."` [targets.ecom] account = "team" project = "3018488" workspace = 3448414 ``` The `[active]` block stores only `account` and (optionally) `workspace` β€” project lives on the active account as `default_project`. Targets are saved cursor positions (see [Saved Targets](#saved-targets) below). ## Managing Accounts via CLI ``` mp account list # All accounts; active marked with * mp account show [NAME] # Account details (omit NAME for active) mp account use NAME # Switch active account (clears workspace) mp account test [NAME] # Probe /me; returns AccountTestResult mp account login NAME # Run PKCE flow (oauth_browser only) mp account logout NAME # Delete on-disk tokens (oauth_browser only) mp account token [NAME] # Print bearer (for piping to curl etc.) mp account update NAME --region eu # Rotate region/secret/token/etc. mp account remove NAME [--force] # Delete; --force orphans dependent targets ``` The first account added auto-promotes to active. ## Managing Accounts via Python The public surface lives in three functional namespaces: ``` import mixpanel_headless as mp # Add a service account mp.accounts.add( "team", type="service_account", region="us", default_project="3018488", username="team-mp...", secret="...", ) # Add an OAuth account and run the browser flow mp.accounts.add("personal", type="oauth_browser", region="us") result = mp.accounts.login("personal") # OAuthLoginResult print(result.user.email, result.expires_at) # List + switch + probe for s in mp.accounts.list(): # list[AccountSummary] print(s.name, s.type, s.region, "*" if s.is_active else "") mp.accounts.use("team") probe = mp.accounts.test() # AccountTestResult print(probe.ok, probe.accessible_project_count) ``` See [API β†’ Auth](https://mixpanel.github.io/mixpanel-headless/api/auth/index.md) for the full namespace reference. ## OAuth (browser) β€” token storage OAuth browser tokens are stored per account; OAuth client metadata (Dynamic Client Registration) is shared per region: ``` ~/.mp/ β”œβ”€β”€ config.toml # accounts, targets, [active] β”œβ”€β”€ accounts/ β”‚ β”œβ”€β”€ personal/ β”‚ β”‚ β”œβ”€β”€ tokens.json # access + refresh tokens (0o600) β”‚ β”‚ └── me.json # cached /me response (0o600) β”‚ └── team/ β”‚ └── me.json └── oauth/ β”œβ”€β”€ client_us.json # shared DCR client per region β”œβ”€β”€ client_eu.json └── client_in.json ``` Tokens auto-refresh on expiry. If the refresh token is rejected (e.g., revoked at the IdP), the next call raises `OAuthError(code="OAUTH_REFRESH_REVOKED")` β€” re-run `mp login --name NAME` to recover (or the legacy `mp account login NAME`). ``` mp account token personal # Print the active access token curl -H "Authorization: Bearer $(mp account token personal)" \ https://mixpanel.com/api/app/me ``` ## Credential Resolution Chain When constructing a `Workspace` (or running a CLI command), each axis is resolved independently. The general chain is: 1. **Environment variables** β€” direct env reads inside the resolver. 1. **Constructor / CLI param** β€” `Workspace(account="...")`, `mp -a NAME ...`. 1. **Saved target** β€” `Workspace(target="ecom")`, `mp -t ecom ...`. 1. **Bridge file** β€” `MP_AUTH_FILE` or `~/.claude/mixpanel/auth.json` (Cowork integration). 1. **Persisted active session** β€” the `[active]` block in `config.toml`. 1. **Account default** β€” `account.default_project` for the project axis. Per-axis details: - **Account** β€” the resolver reads the **service-account env quad** (`MP_USERNAME` + `MP_SECRET` + `MP_PROJECT_ID` + `MP_REGION`) and the **OAuth-token env triple** (`MP_OAUTH_TOKEN` + `MP_PROJECT_ID` + `MP_REGION`) directly. The SA quad wins over the OAuth triple. `MP_ACCOUNT` is wired as the `envvar=` default for `--account` / `-a` by Typer β€” it is **not** read directly by the resolver's env step (unlike `MP_USERNAME` / `MP_OAUTH_TOKEN`). An explicit `--account NAME` or `Workspace(account="...")` overrides it normally. - **Project** β€” `MP_PROJECT_ID` is read directly by the resolver (env layer), then `--project` / `Workspace(project=...)` (param), then target, bridge, and finally the active account's `default_project`. - **Workspace** β€” `MP_WORKSPACE_ID` is read directly by the resolver (env layer), then `--workspace` / `Workspace(workspace=...)` (param), then target, bridge, and `[active].workspace`. There is **no silent cross-axis fallback**: switching the active account clears the workspace (workspaces are project-scoped), and project doesn't carry forward to a new account. If an axis can't be resolved, the resolver raises `ConfigError` rather than silently falling back to a default. ``` import mixpanel_headless as mp # Default β€” resolve everything from config + env ws = mp.Workspace() # Explicit per-axis overrides ws = mp.Workspace(account="team", project="3713224") ws = mp.Workspace(target="ecom") ``` ## Workspace Axis (in-session switching) Workspaces are project-scoped. Set the workspace via env var, CLI flag, constructor arg, or `Workspace.use()`: ``` mp --workspace 3448414 inspect events # one-off override mp workspace use 3448414 # persist to [active].workspace export MP_WORKSPACE_ID=3448414 # env-var override ``` ``` import mixpanel_headless as mp ws = mp.Workspace(workspace=3448414) # at construction ws.use(workspace=3448414) # in-session switch (returns self) ws.use(workspace=3448414, persist=True) # also write to [active] ``` If no workspace is specified, workspace-scoped endpoints lazy-resolve to the project's default workspace on first use. ## Saved Targets A **target** is a saved (account, project, optional workspace) bundle β€” a named cursor position you can apply with one command: ``` mp target add ecom --account team --project 3018488 --workspace 3448414 mp target list # One-off application (CLI override only) mp --target ecom query segmentation -e Login --from 2026-04-01 # Persistent application (writes [active] atomically) mp target use ecom # Active: team β†’ E-Commerce Demo (3018488), workspace 3448414 ``` Python: ``` import mixpanel_headless as mp mp.targets.add("ecom", account="team", project="3018488", workspace=3448414) mp.targets.use("ecom") # writes [active] atomically ws = mp.Workspace(target="ecom") # apply at construction ws.use(target="ecom") # apply in-session ``` `--target` is mutually exclusive with `--account` / `--project` / `--workspace` (and the equivalent constructor kwargs). ## Bridge Files (Cowork integration) The Cowork bridge is a v2 JSON file that lets a remote VM (or any environment that doesn't have your `~/.mp/config.toml`) authenticate against Mixpanel using your host machine's account and tokens. ``` # On the host β€” write the bridge mp account export-bridge --to ~/.claude/mixpanel/auth.json # Wrote bridge: ~/.claude/mixpanel/auth.json # Account: personal (oauth_browser, us) # Tokens: included (refresh-capable) # In the VM β€” bridge is auto-discovered, or override with env var export MP_AUTH_FILE=/host/.claude/mixpanel/auth.json mp project list # works without `mp account add` mp session --bridge # show bridge-resolved state # On the host β€” tear down mp account remove-bridge # Removed bridge: ~/.claude/mixpanel/auth.json ``` The bridge file embeds the full `Account` (with secrets), optional OAuth tokens (for `oauth_browser` accounts), and optional pinned project/workspace/headers. Default search order: `MP_AUTH_FILE` β†’ `~/.claude/mixpanel/auth.json` β†’ `./mixpanel_auth.json`. ``` from pathlib import Path import mixpanel_headless as mp mp.accounts.export_bridge(to=Path("~/.claude/mixpanel/auth.json").expanduser()) mp.accounts.remove_bridge() ``` ## Data Residency Regions Mixpanel stores data in regional data centers. Use the correct region for your project: | Region | Code | API Endpoint | | -------------- | ---- | ----------------- | | United States | `us` | `mixpanel.com` | | European Union | `eu` | `eu.mixpanel.com` | | India | `in` | `in.mixpanel.com` | Region mismatch Using the wrong region results in authentication errors or empty data. The region lives on the account (`account.region`); change it via `mp account update NAME --region eu`. ## Next Steps - [Quick Start](https://mixpanel.github.io/mixpanel-headless/getting-started/quickstart/index.md) β€” Run your first queries - [API β†’ Auth](https://mixpanel.github.io/mixpanel-headless/api/auth/index.md) β€” Full Python API reference for accounts, sessions, and targets - [CLI Reference](https://mixpanel.github.io/mixpanel-headless/cli/index.md) β€” Complete CLI command reference Copy markdown # User Guide # The Unified Query System Five analytics engines. One Python vocabulary. Every query validated before it hits the network. ``` import mixpanel_headless as mp ws = mp.Workspace() ws.query("Login") # Insights ws.query_funnel(["Signup", "Purchase"]) # Funnels ws.query_retention("Signup", "Login") # Retention ws.query_flow("Purchase") # Flows ws.query_user(where=Filter.is_set("$email")) # Users ``` Each method returns a typed result with a lazy `.df` property. Each method validates every parameter before making an API call. And the keyword arguments you learn for one method work in all the others. ______________________________________________________________________ ## The pattern Every query method shares the same vocabulary: ``` # These keywords mean the same thing everywhere result = ws.query( "Login", where=Filter.equals("country", "US"), # filter group_by="platform", # breakdown last=90, # time range time_comparison="previous_period", # compare periods data_group_id=42, # data group scope ) result = ws.query_funnel( ["Signup", "Purchase"], where=Filter.equals("country", "US"), # same group_by="platform", # same last=90, # same time_comparison="previous_period", # same data_group_id=42, # same ) result = ws.query_retention( "Signup", "Login", where=Filter.equals("country", "US"), # same group_by="platform", # same last=90, # same time_comparison="previous_period", # same data_group_id=42, # same ) result = ws.query_flow( "Purchase", where=Filter.equals("country", "US"), # property filters supported last=90, # same data_group_id=42, # same ) ``` Learn `Filter`, `GroupBy`, `where=`, `group_by=`, `last=`, `time_comparison=`, and `data_group_id=` once. Use them across engines (flows has some restrictions β€” see below). ______________________________________________________________________ ## Strings first, objects when you need them Every parameter that accepts a typed object also accepts a plain string. Start simple, upgrade to objects when you need more control: ``` # Strings β€” simple and readable ws.query("Login", group_by="country") # GroupBy object β€” when you need numeric bucketing ws.query( "Purchase", group_by=GroupBy( "revenue", property_type="number", bucket_size=50, ), ) ``` ``` # Strings β€” just event names ws.query_funnel(["Signup", "Purchase"]) # FunnelStep objects β€” when you need per-step filters ws.query_funnel([ FunnelStep("Signup"), FunnelStep("Purchase", filters=[Filter.greater_than("amount", 50)]), ]) ``` ``` # Strings β€” just born and return events ws.query_retention("Signup", "Login") # RetentionEvent objects β€” when you need per-event filters ws.query_retention( RetentionEvent("Signup", filters=[Filter.equals("source", "organic")]), "Login", ) ``` ``` # String β€” just an anchor event ws.query_flow("Purchase") # FlowStep object β€” per-step direction or filters ws.query_flow( FlowStep( "Purchase", forward=5, reverse=2, filters=[Filter.greater_than("amount", 50)], ), ) ``` Mix freely. Strings and objects can appear in the same query. **Note:** `FlowStep.filters` accepts any `Filter` type for per-step filtering. The query-level `where=` parameter on `query_flow()` accepts both cohort filters (`Filter.in_cohort` / `Filter.not_in_cohort`) and property filters (`Filter.equals()`, `Filter.greater_than()`, etc.). ______________________________________________________________________ ## Filters: typed methods, not operator strings Every filter is a class method on `Filter`. Autocomplete shows you every option: ``` from mixpanel_headless import Filter # String comparisons Filter.equals("country", "US") Filter.equals("country", ["US", "CA", "UK"]) # multi-value Filter.not_equals("status", "banned") Filter.contains("email", "@company.com") # Numeric comparisons Filter.greater_than("amount", 100) Filter.less_than("age", 18) Filter.between("revenue", 50, 500) # Existence Filter.is_set("utm_source") Filter.is_not_set("phone") # Boolean Filter.is_true("is_premium") Filter.is_false("opted_out") # Dates Filter.in_the_last("created", 30, "day") Filter.before("signup_date", "2025-01-01") # Cohorts (see "Cohort scoping" below) Filter.in_cohort(123, "Power Users") Filter.not_in_cohort(456, "Bots") ``` Combine multiple filters with `where=`: ``` # AND logic (default) β€” all conditions must match result = ws.query("Purchase", where=[ Filter.equals("country", "US"), Filter.greater_than("amount", 25), Filter.is_true("is_premium"), ]) ``` Filters work identically across `query()`, `query_funnel()`, `query_retention()`, and `query_flow()`. ______________________________________________________________________ ## Results: DataFrames, params, and metadata Insights, funnel, and retention results share a common structure: ``` result = ws.query("Login", math="dau", last=30) result.df # pandas DataFrame β€” lazy, cached result.params # the exact bookmark JSON sent to the API result.from_date # resolved start date result.to_date # resolved end date result.computed_at # when Mixpanel computed the result result.meta # sampling factor, cache status ``` Flow results have `computed_at`, `params`, and `meta` but not `from_date`/`to_date` β€” flow data is structured around nodes, edges, and trees instead. Engine-specific results add domain-relevant properties: ``` # Funnels result = ws.query_funnel(["Signup", "Purchase"]) result.overall_conversion_rate # 0.12 result.steps_data # per-step counts and ratios # Retention result = ws.query_retention("Signup", "Login") result.cohorts # per-cohort-date retention data result.average # synthetic average # Flows (sankey mode) result = ws.query_flow("Purchase") result.nodes_df # step | event | type | count result.edges_df # source -> target with counts result.graph # NetworkX DiGraph result.top_transitions(5) # highest-traffic edges result.drop_off_summary() # per-step drop-off rates # Flows (tree mode) result = ws.query_flow("Purchase", mode="tree") result.trees # recursive FlowTreeNode objects result.anytree # anytree AnyNode objects ``` ______________________________________________________________________ ## The five engines ### Insights β€” `query()` The general-purpose analytics engine. Counts, aggregations, DAU/WAU/MAU, formulas, rolling windows. ``` from mixpanel_headless import Metric, Formula, Filter, GroupBy # DAU over 90 days, weekly result = ws.query("Login", math="dau", last=90, unit="week") # Revenue percentiles result = ws.query("Purchase", math="p99", math_property="amount") # Per-user average purchases result = ws.query( "Purchase", math="total", per_user="average", math_property="amount", ) # 7-day rolling average result = ws.query("Signup", math="unique", rolling=7, last=60) # Multi-event comparison result = ws.query(["Signup", "Login", "Purchase"], math="unique") ``` #### Formulas Compute derived metrics. Letters A-Z reference events by position: ``` # Top-level formula parameter result = ws.query( [ Metric("Signup", math="unique"), Metric("Purchase", math="unique"), ], formula="(B / A) * 100", formula_label="Conversion Rate", unit="week", ) # Or Formula objects in the event list result = ws.query([ Metric("Signup", math="unique"), Metric("Purchase", math="unique"), Formula("(B / A) * 100", label="Conversion Rate"), ]) ``` #### The Metric class When different events need different aggregation: ``` result = ws.query([ Metric("Signup", math="unique"), Metric("Purchase", math="total", property="revenue"), Metric( "Support Ticket", math="unique", filters=[Filter.equals("priority", "high")], ), ]) ``` **Full reference:** [Insights Queries](https://mixpanel.github.io/mixpanel-headless/guide/query/index.md) ______________________________________________________________________ ### Funnels β€” `query_funnel()` Step-by-step conversion analysis with conversion windows, exclusions, and step ordering. ``` from mixpanel_headless import ( FunnelStep, Exclusion, HoldingConstant, Filter, ) # Two-step funnel with 7-day conversion window result = ws.query_funnel( ["Signup", "Purchase"], conversion_window=7, ) print(f"Conversion: {result.overall_conversion_rate:.1%}") # Per-step filters and labels result = ws.query_funnel([ FunnelStep("Signup"), FunnelStep( "Add to Cart", filters=[Filter.greater_than("item_count", 0)], ), FunnelStep("Checkout"), FunnelStep("Purchase", label="Completed Purchase"), ]) # Exclude events between steps result = ws.query_funnel( ["Signup", "Add to Cart", "Purchase"], exclusions=["Logout"], # all steps # Or target a specific range: # exclusions=[Exclusion("Refund", from_step=1, to_step=2)], ) # Hold a property constant across all steps result = ws.query_funnel( ["Signup", "Purchase"], # users must complete on same platform holding_constant="platform", ) # Session-based conversion result = ws.query_funnel( ["Browse", "Add to Cart", "Purchase"], conversion_window=1, conversion_window_unit="session", math="conversion_rate_session", ) # Funnel trends over time result = ws.query_funnel( ["Signup", "Purchase"], mode="trends", last=90, unit="week", ) ``` **Full reference:** [Funnel Queries](https://mixpanel.github.io/mixpanel-headless/guide/query-funnels/index.md) ______________________________________________________________________ ### Retention β€” `query_retention()` Cohort retention with custom buckets, alignment modes, and display options. ``` from mixpanel_headless import RetentionEvent, Filter # Weekly retention, last 90 days result = ws.query_retention( "Signup", "Login", retention_unit="week", last=90, ) # Average retention curve avg = result.average for i, rate in enumerate(avg["rates"]): print(f" Week {i}: {rate:.1%}") # Custom retention milestones result = ws.query_retention( "Signup", "Login", retention_unit="day", bucket_sizes=[1, 3, 7, 14, 30, 60, 90], last=90, ) # Per-event filters result = ws.query_retention( RetentionEvent( "Signup", filters=[Filter.equals("source", "organic")], ), RetentionEvent( "Purchase", filters=[Filter.greater_than("amount", 0)], ), retention_unit="month", last=180, ) # Retention trends over time result = ws.query_retention( "Signup", "Login", mode="trends", unit="week", last=180, ) ``` **Full reference:** [Retention Queries](https://mixpanel.github.io/mixpanel-headless/guide/query-retention/index.md) ______________________________________________________________________ ### Flows β€” `query_flow()` Path analysis with forward/reverse tracing, three visualization modes, and graph algorithms. ``` from mixpanel_headless import FlowStep, Filter # What happens after Purchase? result = ws.query_flow("Purchase", forward=5) # What leads to Cancel Subscription? result = ws.query_flow("Cancel Subscription", forward=0, reverse=5) # Both directions result = ws.query_flow("Add to Cart", forward=3, reverse=2) # Hide noisy events, increase path variety result = ws.query_flow( "Purchase", hidden_events=[ "Session Start", "Page View", "Heartbeat", ], cardinality=10, collapse_repeated=True, last=90, ) ``` #### Three visualization modes ``` # Sankey (default) β€” aggregated node/edge graph result = ws.query_flow("Purchase", mode="sankey") print(result.nodes_df) print(result.edges_df) g = result.graph # NetworkX DiGraph # Paths β€” top user paths as sequences result = ws.query_flow("Purchase", mode="paths") print(result.df) # Tree β€” recursive decision tree from anchor result = ws.query_flow("Purchase", mode="tree") for tree in result.trees: print(tree.render()) # ASCII visualization ``` #### NetworkX graph The sankey-mode graph unlocks the full NetworkX algorithm library: ``` import networkx as nx g = result.graph # Shortest path from Signup to Purchase path = nx.shortest_path(g, "Signup@0", "Purchase@3") # Biggest bottleneck β€” highest betweenness centrality betweenness = nx.betweenness_centrality(g, weight="count") bottleneck = max(betweenness, key=betweenness.get) # Micro-conversion between any two steps cart_out = sum( d["count"] for _, _, d in g.out_edges("Add to Cart@2", data=True) ) to_purchase = g.edges["Add to Cart@2", "Purchase@3"]["count"] print(f"Cart β†’ Purchase: {to_purchase / cart_out:.1%}") # Dead ends β€” reachable but leading nowhere dead_ends = [ n for n in g.nodes() if g.out_degree(n) == 0 and g.in_degree(n) > 0 ] ``` #### Tree traversal Tree mode gives per-node conversion and drop-off counts: ``` result = ws.query_flow("Signup", mode="tree", forward=4) for tree in result.trees: # At each fork, what % takes each branch? for node in tree.flatten(): if node.children: total = node.total_count print(f"\nAfter {node.event} ({total} users):") ranked = sorted( node.children, key=lambda c: c.total_count, reverse=True, ) for child in ranked: pct = child.total_count / total * 100 print(f" -> {child.event}: {pct:.0f}%") # Best path through the product best = max( tree.all_paths(), key=lambda p: p[-1].converted_count, ) print(" -> ".join( f"{n.event}({n.conversion_rate:.0%})" for n in best )) # Biggest single drop-off worst = max( tree.flatten(), key=lambda n: n.drop_off_count, ) print(f"Biggest drop-off: {worst.event} " f"({worst.drop_off_count} users lost)") ``` Trees also convert to [anytree](https://anytree.readthedocs.io/) nodes for rendering, export, and Graphviz: ``` from anytree import RenderTree from anytree.exporter import UniqueDotExporter root = result.anytree[0] for pre, _, node in RenderTree(root): print(f"{pre}{node.event} ({node.total_count})") UniqueDotExporter(root).to_picture("flow.png") ``` **Full reference:** [Flow Queries](https://mixpanel.github.io/mixpanel-headless/guide/query-flows/index.md) ______________________________________________________________________ ### Users β€” `query_user()` Search, filter, sort, and aggregate user profiles stored in Mixpanel. Default mode is `"aggregate"` β€” use `mode="profiles"` to fetch individual records. ``` from mixpanel_headless import Filter # Find premium users, sorted by lifetime value result = ws.query_user( mode="profiles", where=Filter.equals("plan", "premium"), properties=["$email", "$name", "ltv"], sort_by="ltv", sort_order="descending", limit=50, ) print(f"{result.total} premium users") print(result.df) # Count profiles matching a condition (aggregate is the default mode) count = ws.query_user(where=Filter.is_set("$email")) print(f"Users with email: {count.value}") ``` **Full reference:** [User Profile Queries](https://mixpanel.github.io/mixpanel-headless/guide/query-users/index.md) ______________________________________________________________________ ## Cross-cutting capabilities These features work across multiple engines through the same parameters. ### Recent additions | Feature | Engines | Description | | ---------------------------- | ----------------------------------- | ------------------------------------------------------------------------------------------ | | `time_comparison=` | Insights, Funnels, Retention | Compare the current period against a previous period | | `data_group_id=` | Insights, Funnels, Retention, Flows | Scope queries to a specific data group | | Property filters in `where=` | Flows | Flows now support property filters (e.g., `Filter.equals()`) in addition to cohort filters | | `segments=` | Flows | Break down flow paths by property, cohort, or frequency | | `exclusions=` | Flows | Hide specific events from flow paths | ### Cohort scoping Three ways to use cohorts β€” all accept either a saved cohort ID or an inline `CohortDefinition`: ``` from mixpanel_headless import ( Filter, CohortBreakdown, CohortMetric, CohortCriteria, CohortDefinition, ) # Define a cohort inline β€” no UI, no saving, no ID to look up power_users = CohortDefinition( CohortCriteria.did_event("Purchase", at_least=3, within_days=30) ) ``` #### 1. Filter β€” restrict a query to a segment ``` # Saved cohort result = ws.query( "Login", where=Filter.in_cohort(123, "Power Users"), ) # Inline cohort result = ws.query( "Login", where=Filter.in_cohort(power_users, name="Power Users"), ) # Exclude a cohort result = ws.query( "Login", where=Filter.not_in_cohort(456, "Bots"), ) # Works in all five engines result = ws.query_funnel( ["Signup", "Purchase"], where=Filter.in_cohort(power_users, name="PU"), ) result = ws.query_retention( "Signup", "Login", where=Filter.in_cohort(123, "PU"), ) result = ws.query_flow( "Purchase", where=Filter.in_cohort(123, "PU"), ) ``` #### 2. Breakdown β€” compare a segment against everyone else ``` result = ws.query( "Purchase", group_by=CohortBreakdown(123, "Power Users"), ) # Two segments: "Power Users" and "Not In Power Users" # Show only the cohort (no negation segment) result = ws.query( "Purchase", group_by=CohortBreakdown( 123, "PU", include_negated=False, ), ) # Combine with property breakdowns result = ws.query( "Purchase", group_by=[CohortBreakdown(123, "PU"), "platform"], ) ``` Works with `query()`, `query_funnel()`, and `query_retention()`. #### 3. Metric β€” track cohort size over time ``` # How many power users do we have each week? result = ws.query( CohortMetric(123, "Power Users"), last=90, unit="week", ) # What % of active users are power users? result = ws.query( [ Metric("Login", math="unique"), CohortMetric(123, "Power Users"), ], formula="(B / A) * 100", formula_label="Power User %", ) ``` Works with `query()` only. #### Engine compatibility | Capability | `query()` | `query_funnel()` | `query_retention()` | `query_flow()` | `query_user()` | | --------------------- | --------- | ---------------- | ------------------- | -------------- | -------------- | | **Cohort Filters** | yes | yes | yes | yes | yes | | **Cohort Breakdowns** | yes | yes | yes | -- | -- | | **Cohort Metrics** | yes | -- | -- | -- | -- | ______________________________________________________________________ ### Custom properties Use saved custom properties by ID or define computed properties inline at query time: ``` from mixpanel_headless import ( CustomPropertyRef, InlineCustomProperty, GroupBy, Filter, Metric, ) # Reference a saved custom property ref = CustomPropertyRef(42) # Or define one inline β€” no UI, no saving revenue = InlineCustomProperty.numeric( "A * B", A="price", B="quantity", ) ``` Custom properties plug into the same parameters you already know: ``` # Breakdown result = ws.query( "Purchase", group_by=GroupBy( property=revenue, property_type="number", bucket_size=100, ), ) # Filter result = ws.query( "Purchase", where=Filter.greater_than(property=ref, value=100), ) # Measurement result = ws.query( Metric("Purchase", math="average", property=ref), ) # Mix with regular properties result = ws.query( "Purchase", group_by=[ "country", GroupBy( property=revenue, property_type="number", bucket_size=50, ), ], where=[ Filter.equals("platform", "iOS"), Filter.greater_than(property=ref, value=100), ], ) ``` Works with `query()`, `query_funnel()`, and `query_retention()`. ______________________________________________________________________ ### Breakdowns `group_by=` accepts strings, `GroupBy` objects, `CohortBreakdown` objects, and lists mixing all three: ``` # String β€” simple property breakdown result = ws.query("Login", group_by="platform") # Multiple properties result = ws.query("Purchase", group_by=["country", "platform"]) # Numeric bucketing result = ws.query( "Purchase", group_by=GroupBy( "revenue", property_type="number", bucket_size=50, bucket_min=0, bucket_max=500, ), ) # Cohort breakdown result = ws.query( "Purchase", group_by=CohortBreakdown(123, "Power Users"), ) # Custom property breakdown result = ws.query( "Purchase", group_by=GroupBy( property=CustomPropertyRef(42), property_type="number", ), ) # Mix all types result = ws.query( "Purchase", group_by=[ "country", GroupBy( "revenue", property_type="number", bucket_size=50, ), CohortBreakdown(123, "Power Users"), ], ) ``` ______________________________________________________________________ ### Time ranges `last=` and absolute dates (`from_date`/`to_date`) work in all five engines. The `unit=` parameter applies to insights, funnels, and retention (flows and users have no `unit=`). ``` # Relative β€” last N days (default: 30) result = ws.query("Login", last=90) result = ws.query("Login", last=12, unit="week") # Absolute β€” explicit dates result = ws.query( "Login", from_date="2025-01-01", to_date="2025-03-31", ) # Hourly granularity (insights only) result = ws.query("Login", last=2, unit="hour") ``` ______________________________________________________________________ ## Validation Every query is validated **before** the API call. Invalid parameters raise `BookmarkValidationError` with all errors at once β€” no "fix one, discover the next" cycle: ``` from mixpanel_headless import BookmarkValidationError try: ws.query_funnel([""], conversion_window=-1) except BookmarkValidationError as e: for error in e.errors: print(f"[{error.code}] {error.path}: {error.message}") if error.suggestion: print(f" Did you mean: {error.suggestion}") # [F2_EMPTY_STEP_EVENT] steps[0].event: # Step event name must be a non-empty string # [F1_MIN_STEPS] steps: # At least 2 steps are required (got 1) # [F3_CONVERSION_WINDOW_POSITIVE] conversion_window: # conversion_window must be a positive integer ``` Each error includes: | Field | What it is | | ------------ | -------------------------------------------------- | | `code` | Machine-readable rule ID (e.g., `V1`, `F3`, `R6`) | | `path` | JSONPath-like location (e.g., `show[0].math`) | | `message` | Human-readable description | | `severity` | `"error"` or `"warning"` | | `suggestion` | Fuzzy-matched alternatives for invalid enum values | | `fix` | Suggested fix payload for programmatic correction | You can also validate insights, funnel, or retention bookmark JSON directly (flow params are validated internally by `query_flow()`): ``` from mixpanel_headless import validate_bookmark errors = validate_bookmark(some_bookmark_dict) ``` ______________________________________________________________________ ## Inspect and persist: `build_*_params()` Every `query_*()` method has a corresponding `build_*_params()` that generates and validates the bookmark JSON without executing the query: ``` # Inspect what would be sent to the API params = ws.build_params( "Login", math="dau", group_by="platform", last=90, ) params = ws.build_funnel_params( ["Signup", "Purchase"], conversion_window=7, ) params = ws.build_retention_params( "Signup", "Login", retention_unit="week", ) params = ws.build_flow_params( "Purchase", forward=3, reverse=1, ) import json print(json.dumps(params, indent=2)) ``` Save any query as a Mixpanel report: ``` from mixpanel_headless import CreateBookmarkParams result = ws.query("Login", math="dau", group_by="platform", last=90) ws.create_bookmark(CreateBookmarkParams( name="DAU by Platform (90d)", bookmark_type="insights", params=result.params, )) ``` This works for all five engines. Build params in code, persist them as reports visible in the Mixpanel UI. ______________________________________________________________________ ## One query, eight capabilities Here's the single query that best demonstrates what the unified system can do. The business question it answers: *"What's the ARPU among activated users, by pricing plan, as a 4-week rolling average over the last quarter?"* Every B2B SaaS PM asks this. In the Mixpanel UI it requires creating a saved custom property, creating a saved cohort, building a two-metric report with a formula, adding a breakdown, and configuring rolling analysis β€” minimum 10 clicks across 3 screens, plus two permanent entities to manage. Here it's one call with zero pre-saved entities: ``` from mixpanel_headless import ( Workspace, Metric, Filter, InlineCustomProperty, CohortCriteria, CohortDefinition, ) ws = Workspace() # Revenue doesn't exist in the data β€” compute it revenue = InlineCustomProperty.numeric( "A * B", A="price", B="quantity", ) # Define the target segment in code activated = CohortDefinition( CohortCriteria.did_event( "Complete Onboarding", at_least=1, within_days=14, ) ) # One call. Eight capabilities. result = ws.query( [ Metric( "Purchase", math="total", property=revenue, # inline custom property ), Metric("Purchase", math="unique"), ], formula="(A / B)", # ARPU formula formula_label="ARPU", where=Filter.in_cohort( # inline cohort filter activated, name="Activated", ), group_by="plan", # breakdown by plan tier rolling=4, # 4-week rolling average unit="week", last=90, ) print(result.df) # DataFrame with date, event, count columns: # date event count # 0 2025-01-06 ARPU | Free 12.50 # 1 2025-01-06 ARPU | Pro 87.30 # 2 2025-01-06 ARPU | Ent 245.00 # ... # Happy with it? Save as a Mixpanel report: from mixpanel_headless import CreateBookmarkParams ws.create_bookmark(CreateBookmarkParams( name="ARPU by Plan (Activated Users)", bookmark_type="insights", params=result.params, )) ``` What each line proves: | Feature | What it demonstrates | | --------------------------------------- | -------------------------------------------- | | `InlineCustomProperty.numeric(...)` | Compute values that don't exist in your data | | `CohortDefinition(CohortCriteria(...))` | Define segments in code, no UI trip | | `Metric(..., property=revenue)` | Custom properties plug into standard params | | Two `Metric` objects | Different aggregation per metric | | `formula="(A / B)"` | Derived KPIs from multiple metrics | | `Filter.in_cohort(activated, ...)` | Scope to a programmatic segment | | `group_by="plan"` | Segment by the dimension that matters | | `rolling=4` | Smooth weekly noise into a trend | Change `within_days=14` to `within_days=7` and re-run. Swap `"plan"` for `"country"`. Change the formula to `A * B * (1 - C)` to add discounts. Each iteration is instant β€” no UI round-trips, no saved entities to update. ______________________________________________________________________ ## Putting it all together A complete analysis combining multiple engines: ``` import mixpanel_headless as mp from mixpanel_headless import ( Metric, Formula, Filter, GroupBy, FunnelStep, Exclusion, RetentionEvent, CohortCriteria, CohortDefinition, CohortBreakdown, InlineCustomProperty, CustomPropertyRef, CreateBookmarkParams, ) ws = mp.Workspace() # --- Reusable segments and computed properties --- premium_users = CohortDefinition( CohortCriteria.did_event( "Upgrade", at_least=1, within_days=365, ) ) revenue = InlineCustomProperty.numeric( "A * B", A="price", B="quantity", ) # --- Insights: revenue trends by country --- daily_revenue = ws.query( Metric("Purchase", math="total", property=revenue), group_by="country", last=90, unit="week", ) print(daily_revenue.df) # --- Insights: conversion rate formula --- conversion = ws.query( [ Metric("Signup", math="unique"), Metric("Purchase", math="unique"), ], formula="(B / A) * 100", formula_label="Conversion Rate", where=Filter.in_cohort( premium_users, name="Premium", ), unit="week", last=90, ) print(conversion.df) # --- Funnels: checkout flow, premium vs. everyone --- checkout = ws.query_funnel( [ FunnelStep("Browse"), FunnelStep( "Add to Cart", filters=[Filter.greater_than("item_count", 0)], ), FunnelStep("Checkout"), FunnelStep("Purchase"), ], conversion_window=7, exclusions=["Logout"], group_by=CohortBreakdown( premium_users, name="Premium Users", ), last=90, ) print(f"Overall: {checkout.overall_conversion_rate:.1%}") print(checkout.df) # --- Retention: do organic signups retain better? --- bucket_sizes = [1, 3, 7, 14, 30] organic_retention = ws.query_retention( RetentionEvent( "Signup", filters=[Filter.equals("source", "organic")], ), "Login", retention_unit="day", bucket_sizes=bucket_sizes, last=90, ) avg = organic_retention.average for day, rate in zip(bucket_sizes, avg["rates"]): print(f" Day {day}: {rate:.1%}") # --- Flows: what do users do after failed checkout? --- failed_checkout = ws.query_flow( "Checkout Error", forward=4, hidden_events=["Session Start", "Page View"], cardinality=10, last=90, ) for src, tgt, count in failed_checkout.top_transitions(5): print(f" {src} -> {tgt}: {count:,}") # Tree mode: where do users diverge? tree_result = ws.query_flow( "Checkout Error", mode="tree", forward=4, ) for tree in tree_result.trees: print(tree.render()) # --- Save any result as a Mixpanel report --- ws.create_bookmark(CreateBookmarkParams( name="Checkout Funnel (Premium Segment)", bookmark_type="funnels", params=checkout.params, )) ``` ______________________________________________________________________ ## Quick reference ### Methods | Method | Engine | Positional args | | ------------------- | --------- | -------------------------------------------------------- | | `query()` | Insights | `events` β€” str, Metric, Formula, or list | | `query_funnel()` | Funnels | `steps` β€” list of str or FunnelStep | | `query_retention()` | Retention | `born_event`, `return_event` | | `query_flow()` | Flows | `event` β€” str, FlowStep, or list | | `query_user()` | Users | keyword-only β€” `where`, `properties`, `sort_by`, `limit` | Each has a matching `build_*_params()` that returns the validated params dict without querying. ### Shared parameters | Parameter | Type | Default | Engines | | ------------------ | ----------------------------------- | ------- | ----------- | | `where=` | `Filter \| list[Filter]` | `None` | All | | `group_by=` | `str \| GroupBy \| CohortBreakdown` | `None` | I, F, R | | `last=` | `int` | `30` | I, F, R, Fl | | `from_date=` | `str` (YYYY-MM-DD) | `None` | I, F, R, Fl | | `to_date=` | `str` (YYYY-MM-DD) | `None` | I, F, R, Fl | | `unit=` | `"day" \| "week" \| "month"` | `"day"` | I, F, R | | `time_comparison=` | `str` | `None` | I, F, R | | `data_group_id=` | `int` | `None` | I, F, R, Fl | | `mode=` | engine-specific | varies | All | | `math=` | engine-specific | varies | I, F, R | | `math_property=` | `str` | `None` | I, F | I = Insights, F = Funnels, R = Retention ### Engine-specific parameters | Engine | Unique parameters | | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | | **Insights** | `per_user`, `percentile_value`, `formula`, `formula_label`, `rolling`, `cumulative` | | **Funnels** | `conversion_window`, `conversion_window_unit`, `order`, `exclusions`, `holding_constant` | | **Retention** | `retention_unit`, `alignment`, `bucket_sizes` | | **Flows** | `forward`, `reverse`, `count_type`, `cardinality`, `collapse_repeated`, `hidden_events`, `segments`, `exclusions` | | **Users** | `properties`, `sort_by`, `sort_order`, `limit`, `mode`, `aggregate`, `aggregate_property`, `percentile`, `segment_by`, `parallel`, `workers` | ### Result types | Engine | Result type | Key properties | | --------- | ---------------------- | -------------------------------------------- | | Insights | `QueryResult` | `.df`, `.series`, `.params` | | Funnels | `FunnelQueryResult` | `.df`, `.overall_conversion_rate` | | Retention | `RetentionQueryResult` | `.df`, `.cohorts`, `.average` | | Flows | `FlowQueryResult` | `.nodes_df`, `.edges_df`, `.graph`, `.trees` | | Users | `UserQueryResult` | `.df`, `.total`, `.value`, `.params` | ### Imports ``` # Everything you might need from mixpanel_headless import ( # Core Workspace, # Insights Metric, Formula, GroupBy, Filter, MathType, PerUserAggregation, # Funnels FunnelStep, Exclusion, HoldingConstant, FunnelMathType, # Retention RetentionEvent, RetentionMathType, # Flows FlowStep, FlowTreeNode, # Cohorts CohortCriteria, CohortDefinition, CohortBreakdown, CohortMetric, # Custom properties CustomPropertyRef, InlineCustomProperty, PropertyInput, # Persistence CreateBookmarkParams, # Validation validate_bookmark, BookmarkValidationError, ) ``` ______________________________________________________________________ ## Next steps - [Insights Queries](https://mixpanel.github.io/mixpanel-headless/guide/query/index.md) β€” full parameter reference, all 14 math types, per-user aggregation - [Funnel Queries](https://mixpanel.github.io/mixpanel-headless/guide/query-funnels/index.md) β€” step configuration, exclusion targeting, session funnels - [Retention Queries](https://mixpanel.github.io/mixpanel-headless/guide/query-retention/index.md) β€” alignment modes, custom buckets, cohort structure - [Flow Queries](https://mixpanel.github.io/mixpanel-headless/guide/query-flows/index.md) β€” NetworkX graph algorithms, tree traversal, anytree export - [User Profile Queries](https://mixpanel.github.io/mixpanel-headless/guide/query-users/index.md) β€” filtering, sorting, property selection, aggregation - [API Reference: Workspace](https://mixpanel.github.io/mixpanel-headless/api/workspace/index.md) β€” complete method signatures - [API Reference: Types](https://mixpanel.github.io/mixpanel-headless/api/types/index.md) β€” all type definitions Copy markdown # Data Discovery Explore your Mixpanel project's schema before writing queries. Discovery results are cached for the session. Explore on DeepWiki πŸ€– **[Discovery Methods Guide β†’](https://deepwiki.com/mixpanel/mixpanel-headless/3.2.2-discovery-methods)** Ask questions about schema exploration, caching behavior, or how to discover your data landscape. ## Listing Events Get all event names in your project: ``` import mixpanel_headless as mp ws = mp.Workspace() events = ws.events() print(events) # ['Login', 'Purchase', 'Signup', ...] ``` ``` mp inspect events # Filter with jq - get first 5 events mp inspect events --format json --jq '.[:5]' # Find events containing "User" mp inspect events --format json --jq '.[] | select(contains("User"))' ``` Events are returned sorted alphabetically. ## Listing Properties Get properties for a specific event: ``` properties = ws.properties("Purchase") print(properties) # ['amount', 'country', 'product_id', ...] ``` ``` mp inspect properties --event Purchase ``` Properties include both event-specific and common properties. ## Property Values Sample values for a property: ``` # Sample values for a property values = ws.property_values("country", event="Purchase") print(values) # ['US', 'UK', 'DE', 'FR', ...] # Limit results values = ws.property_values("country", event="Purchase", limit=5) ``` ``` mp inspect values --property country --event Purchase --limit 10 ``` ## Subproperties Some Mixpanel event properties are **lists of objects** β€” for example, a `cart` property whose value is `[{"Brand": "nike", "Category": "hats", "Price": 51}, ...]`. The `property_values()` endpoint returns these as JSON-encoded strings, which makes them awkward to inspect by eye. `subproperties()` parses a sample of those blobs and infers a scalar type per inner key. ``` for sp in ws.subproperties("cart", event="Cart Viewed"): print(sp.name, sp.type, sp.sample_values) # Brand string ('nike', 'puma', 'h&m') # Category string ('hats', 'jeans') # Item ID number (35317, 35318) # Price number (51, 87, 102) ``` ``` mp inspect subproperties --property cart --event "Cart Viewed" # Sample more rows (default: 50) mp inspect subproperties -p cart -e "Cart Viewed" --sample-size 200 # Tabular output mp inspect subproperties -p cart -e "Cart Viewed" --format table ``` Results are alphabetically sorted by `name`. Subproperties whose values are themselves dicts/lists are silently skipped (only scalar sub-values are reportable). When a sub-key is observed with mixed scalar shapes, with both scalar and dict shapes, or with only `null` values, the call emits a `UserWarning`. The discovered names and types feed directly into [`Filter.list_contains`](https://mixpanel.github.io/mixpanel-headless/guide/query/#list-of-object-filters) and [`GroupBy.list_item`](https://mixpanel.github.io/mixpanel-headless/guide/query/#list-of-object-breakdowns) for filtering and breaking down by subproperty values. ### SubPropertyInfo ``` sp.name # "Brand" sp.type # "string" | "number" | "boolean" | "datetime" sp.sample_values # ('nike', 'puma', 'h&m') β€” up to 5 distinct values sp.to_dict() # {'name': 'Brand', 'type': 'string', 'sample_values': ['nike', ...]} ``` ## Saved Funnels List funnels defined in Mixpanel: ``` funnels = ws.funnels() for f in funnels: print(f"{f.funnel_id}: {f.name}") ``` ``` mp inspect funnels ``` ### FunnelInfo ``` f.funnel_id # 12345 f.name # "Checkout Funnel" ``` ## Saved Cohorts List cohorts defined in Mixpanel: ``` cohorts = ws.cohorts() for c in cohorts: print(f"{c.id}: {c.name} ({c.count} users)") ``` ``` mp inspect cohorts ``` ### SavedCohort ``` c.id # 12345 c.name # "Power Users" c.count # 5000 c.description # "Users with 10+ logins" c.created # datetime c.is_visible # True ``` ## Lexicon Schemas Retrieve data dictionary schemas for events and profile properties. Schemas include descriptions, property types, and metadata defined in Mixpanel's Lexicon. Schema Coverage The Lexicon API returns only events/properties with explicit schemas (defined via API, CSV import, or UI). It does not return all events visible in Lexicon's UI. Schema Registry CRUD For write operations on the schema registry (create, update, delete schemas and enforcement configuration), see the [Data Governance guide β€” Schema Registry](https://mixpanel.github.io/mixpanel-headless/guide/data-governance/#schema-registry). ``` # List all schemas schemas = ws.lexicon_schemas() for s in schemas: print(f"{s.entity_type}: {s.name}") # Filter by entity type event_schemas = ws.lexicon_schemas(entity_type="event") profile_schemas = ws.lexicon_schemas(entity_type="profile") # Get a specific schema schema = ws.lexicon_schema("event", "Purchase") print(schema.schema_json.description) for prop, info in schema.schema_json.properties.items(): print(f" {prop}: {info.type}") ``` ``` mp inspect lexicon-schemas mp inspect lexicon-schemas --type event mp inspect lexicon-schemas --type profile mp inspect lexicon-schema --type event --name Purchase ``` ### LexiconSchema ``` s.entity_type # "event", "profile", or other API-returned types s.name # "Purchase" s.schema_json # LexiconDefinition object ``` ### LexiconDefinition ``` s.schema_json.description # "User completes a purchase" s.schema_json.properties # dict[str, LexiconProperty] s.schema_json.metadata # LexiconMetadata or None ``` ### LexiconProperty ``` prop = s.schema_json.properties["amount"] prop.type # "number" prop.description # "Purchase amount in USD" prop.metadata # LexiconMetadata or None ``` ### LexiconMetadata ``` meta = s.schema_json.metadata meta.display_name # "Purchase Event" meta.tags # ["core", "revenue"] meta.hidden # False meta.dropped # False meta.contacts # ["owner@company.com"] meta.team_contacts # ["Analytics Team"] ``` Rate Limit The Lexicon API has a strict rate limit of **5 requests per minute**. Schema results are cached for the session to minimize API calls. Write Operations The Lexicon schemas shown here are **read-only discovery** methods. For full CRUD operations on Lexicon definitions (update, delete events/properties, manage tags, bulk updates), see the [Data Governance guide](https://mixpanel.github.io/mixpanel-headless/guide/data-governance/index.md). ## Top Events Get today's most active events: ``` # General top events top = ws.top_events(type="general") for event in top: print(f"{event.event}: {event.count} ({event.percent_change:+.1f}%)") # Average top events top = ws.top_events(type="average", limit=5) ``` ``` mp inspect top-events --type general --limit 10 ``` ### TopEvent ``` event.event # "Login" event.count # 15000 event.percent_change # 12.5 (compared to yesterday) ``` Not Cached Unlike other discovery methods, `top_events()` always makes an API call since it returns real-time data. ## JQL-Based Remote Discovery These methods use JQL (JavaScript Query Language) to analyze data directly on Mixpanel's servers, returning aggregated results without fetching raw data locally. ### Property Value Distribution Understand what values a property contains and how often they appear: ``` result = ws.property_distribution( event="Purchase", property="country", from_date="2025-01-01", to_date="2025-01-31", limit=10, ) print(f"Total: {result.total_count}") for v in result.values: print(f" {v.value}: {v.count} ({v.percentage:.1f}%)") ``` ``` mp inspect distribution -e Purchase -p country --from 2025-01-01 --to 2025-01-31 mp inspect distribution -e Purchase -p country --from 2025-01-01 --to 2025-01-31 --limit 10 ``` ### Numeric Property Summary Get statistical summary for numeric properties: ``` result = ws.numeric_summary( event="Purchase", property="amount", from_date="2025-01-01", to_date="2025-01-31", ) print(f"Count: {result.count}") print(f"Range: {result.min} to {result.max}") print(f"Avg: {result.avg:.2f}, Stddev: {result.stddev:.2f}") print(f"Median: {result.percentiles[50]}") ``` ``` mp inspect numeric -e Purchase -p amount --from 2025-01-01 --to 2025-01-31 mp inspect numeric -e Purchase -p amount --from 2025-01-01 --to 2025-01-31 --percentiles 25,50,75,90 ``` ### Daily Event Counts See event activity over time: ``` result = ws.daily_counts( from_date="2025-01-01", to_date="2025-01-07", events=["Purchase", "Signup"], ) for c in result.counts: print(f"{c.date} {c.event}: {c.count}") ``` ``` mp inspect daily --from 2025-01-01 --to 2025-01-07 mp inspect daily --from 2025-01-01 --to 2025-01-07 -e Purchase,Signup ``` ### User Engagement Distribution Understand how engaged users are by their event count: ``` result = ws.engagement_distribution( from_date="2025-01-01", to_date="2025-01-31", ) print(f"Total users: {result.total_users}") for b in result.buckets: print(f" {b.bucket_label} events: {b.user_count} ({b.percentage:.1f}%)") ``` ``` mp inspect engagement --from 2025-01-01 --to 2025-01-31 mp inspect engagement --from 2025-01-01 --to 2025-01-31 --buckets 1,5,10,50,100 ``` ### Property Coverage Check data quality by seeing how often properties are defined: ``` result = ws.property_coverage( event="Purchase", properties=["coupon_code", "referrer", "utm_source"], from_date="2025-01-01", to_date="2025-01-31", ) print(f"Total events: {result.total_events}") for c in result.coverage: print(f" {c.property}: {c.coverage_percentage:.1f}% defined") ``` ``` mp inspect coverage -e Purchase -p coupon_code,referrer,utm_source --from 2025-01-01 --to 2025-01-31 ``` When to Use JQL-Based Discovery These methods are ideal for: - **Quick exploration**: Understand data shape before fetching locally - **Large date ranges**: Analyze months of data without downloading everything - **Data quality checks**: Verify property coverage and value distributions - **Trend analysis**: See daily activity patterns See the [JQL Discovery Types](https://mixpanel.github.io/mixpanel-headless/api/types/#jql-discovery-types) in the API reference for return type details. ## Caching Discovery results are cached for the lifetime of the Workspace: ``` ws = mp.Workspace() # First call hits the API events1 = ws.events() # Second call returns cached result (instant) events2 = ws.events() # Clear cache to force refresh ws.clear_discovery_cache() # Now hits API again events3 = ws.events() ``` ## Discovery Workflow A typical discovery workflow before analysis: ``` import mixpanel_headless as mp ws = mp.Workspace() # 1. What events exist? print("Events:") for event in ws.events()[:10]: print(f" - {event}") # 2. What properties does Purchase have? print("\nPurchase properties:") for prop in ws.properties("Purchase"): print(f" - {prop}") # 3. What values does 'country' have? print("\nCountry values:") for value in ws.property_values("country", event="Purchase", limit=10): print(f" - {value}") # 4. What funnels are defined? print("\nFunnels:") for f in ws.funnels(): print(f" - {f.name} (ID: {f.funnel_id})") # 5. Run a live query with discovered data result = ws.segmentation( event="Purchase", from_date="2025-01-01", to_date="2025-01-31", on="country" ) print(result.df) ``` ## Next Steps - [Streaming Data](https://mixpanel.github.io/mixpanel-headless/guide/streaming/index.md) β€” Stream events and profiles - [API Reference](https://mixpanel.github.io/mixpanel-headless/api/workspace/index.md) β€” Complete API documentation Copy markdown # Insights Queries Build typed analytics queries against Mixpanel's Insights engine β€” the same engine that powers the Mixpanel web UI. Recommended `Workspace.query()` is the primary way to run analytics queries programmatically. It supports capabilities not available through the legacy query methods, including DAU/WAU/MAU, multi-metric comparison, formulas, per-user aggregation, rolling windows, and percentiles. ## When to Use `query()` `query()` uses the Insights engine via inline bookmark params. The legacy methods (`segmentation()`, `funnel()`, `retention()`) use the older Query API endpoints. Use `query()` when you need any of the capabilities in the right column: | Capability | Legacy methods | `query()` | | ----------------------------------- | ----------------------------- | -------------------------------------------- | | Simple event count over time | `segmentation()` | `ws.query("Login")` | | Unique users | `segmentation(type="unique")` | `math="unique"` | | DAU / WAU / MAU | Not available | `math="dau"` | | Multi-metric comparison | Not available | `["Signup", "Login", "Purchase"]` | | Formulas (conversion rates, ratios) | Not available | `formula="(B/A)*100"` | | Per-user aggregation | Not available | `per_user="average"` | | Rolling / cumulative analysis | Not available | `rolling=7` | | Percentiles (p25/p75/p90/p99) | Not available | `math="p90"` | | Typed filters | Expression strings | `Filter.equals("country", "US")` | | Numeric bucketed breakdowns | Not available | `GroupBy("revenue", property_type="number")` | | Save query as a report | N/A | `result.params` β†’ `create_bookmark()` | Use the legacy methods when: - You need to query a saved funnel by ID β†’ `funnel()` - You need cohort retention curves β†’ `query_retention()` ([Retention Queries](https://mixpanel.github.io/mixpanel-headless/guide/query-retention/index.md)) - You need raw JQL execution β†’ `jql()` - You need to query a saved Flows report β†’ `query_saved_flows()` For ad-hoc funnel conversion analysis with typed step definitions, see **[Funnel Queries](https://mixpanel.github.io/mixpanel-headless/guide/query-funnels/index.md)**. For ad-hoc flow path analysis with typed step definitions, see **[Flow Queries](https://mixpanel.github.io/mixpanel-headless/guide/query-flows/index.md)**. ## Getting Started The simplest possible query β€” total event count per day for the last 30 days: ``` import mixpanel_headless as mp ws = mp.Workspace() result = ws.query("Login") print(result.df.head()) # date event count # 0 2025-03-01 Login [Total Events] 142 # 1 2025-03-02 Login [Total Events] 158 ``` Add a time range and aggregation: ``` # Unique users per week for the last 7 days result = ws.query("Login", math="unique", last=7, unit="week") # Last 90 days of DAU result = ws.query("Login", math="dau", last=90) # Specific date range result = ws.query( "Purchase", from_date="2025-01-01", to_date="2025-03-31", unit="month", ) ``` ## Aggregation ### Counting | Math type | What it counts | | --------------------- | ----------------------------------------- | | `"total"` (default) | Total event occurrences | | `"unique"` | Unique users per period | | `"dau"` | Daily active users | | `"wau"` | Weekly active users | | `"mau"` | Monthly active users | | `"cumulative_unique"` | Running count of distinct users over time | | `"sessions"` | Session count (not events or users) | ``` # DAU over the last 90 days result = ws.query("Login", math="dau", last=90) # Monthly active users result = ws.query("Login", math="mau", last=6, unit="month") # Cumulative unique users over time result = ws.query("Login", math="cumulative_unique", last=90) # Count sessions instead of events result = ws.query("Login", math="sessions", last=30) # Distinct values of a property result = ws.query("Purchase", math="unique_values", math_property="product_id") ``` ### Property Aggregation Aggregate a numeric property across events. Requires `math_property`: | Math type | Aggregation | | ------------------------------------- | ----------------------------------------- | | `"total"` + `math_property` | Sum of a numeric property | | `"average"` | Mean value | | `"median"` | Median value | | `"min"` / `"max"` | Extremes | | `"p25"` / `"p75"` / `"p90"` / `"p99"` | Percentiles | | `"percentile"` + `percentile_value` | Custom percentile (e.g. p95) | | `"histogram"` | Distribution of property values | | `"unique_values"` | Count of distinct values of a property | | `"most_frequent"` | Most commonly occurring property value | | `"first_value"` | First observed value per user | | `"multi_attribution"` | Multi-touch attribution across a property | | `"numeric_summary"` | Summary stats (count, mean, variance) | ``` # Average purchase amount per day result = ws.query( "Purchase", math="average", math_property="amount", from_date="2025-01-01", to_date="2025-01-31", ) # P90 response time result = ws.query("API Call", math="p90", math_property="duration_ms") # Custom percentile (p95) β€” use math="percentile" with percentile_value result = ws.query( "API Call", math="percentile", math_property="duration_ms", percentile_value=95, ) # Histogram β€” distribution of purchase amounts result = ws.query("Purchase", math="histogram", math_property="amount") ``` ### Per-User Aggregation Aggregate per user first, then across all users β€” like a SQL subquery. For example, "what's the average number of purchases per user per week?" ``` # Average purchases per user per week result = ws.query( "Purchase", math="total", per_user="average", unit="week", ) ``` Valid `per_user` values: `"unique_values"`, `"total"`, `"average"`, `"min"`, `"max"`. Note `per_user` is incompatible with `dau`, `wau`, `mau`, and `unique` math types. ### Segment Method Control how events are counted per user with `Metric.segment_method`: - `"all"` (default) β€” count every qualifying event - `"first"` β€” count only the first qualifying event per user ``` from mixpanel_headless import Metric # Only count each user's first purchase result = ws.query(Metric("Purchase", segment_method="first"), last=30) ``` ### The `Metric` Class When different events need different aggregation settings, use `Metric` objects instead of plain strings: ``` from mixpanel_headless import Metric # Different math per event result = ws.query([ Metric("Signup", math="unique"), Metric("Purchase", math="total", property="revenue"), ]) ``` `Metric` also supports `percentile_value` for custom percentiles: ``` # Per-metric custom percentile result = ws.query( Metric("API Call", math="percentile", property="duration_ms", percentile_value=95), ) ``` Plain strings inherit the top-level `math`, `math_property`, and `per_user` defaults. `Metric` objects override them per-event: ``` # These are equivalent: ws.query("Login", math="unique") ws.query(Metric("Login", math="unique")) # Top-level defaults apply to all string events: ws.query(["Signup", "Login"], math="unique") # Both events use math="unique" # Metric overrides per event: ws.query([ Metric("Signup", math="unique"), # unique users Metric("Purchase", math="total"), # total events ]) ``` ## Filters ### Global Filters Apply filters across all metrics with `where=`. Construct filters using `Filter` class methods: ``` from mixpanel_headless import Filter # Single filter result = ws.query( "Purchase", where=Filter.equals("country", "US"), ) # Multiple filters (combined with AND) result = ws.query( "Purchase", where=[ Filter.equals("country", "US"), Filter.greater_than("amount", 50), ], ) ``` ### Available Filter Methods **String filters:** ``` Filter.equals("browser", "Chrome") # equals value Filter.equals("browser", ["Chrome", "Firefox"]) # equals any in list Filter.not_equals("browser", "Safari") # does not equal Filter.contains("email", "@company.com") # contains substring Filter.not_contains("url", "staging") # does not contain Filter.starts_with("email", "admin") # prefix match Filter.ends_with("email", "@company.com") # suffix match ``` **Numeric filters:** ``` Filter.greater_than("amount", 100) # > 100 Filter.less_than("age", 65) # < 65 Filter.between("amount", 10, 100) # 10 <= x <= 100 Filter.not_between("age", 18, 65) # outside a range Filter.at_least("score", 80) # >= 80 Filter.at_most("errors", 5) # <= 5 ``` **Existence filters:** ``` Filter.is_set("phone_number") # property exists Filter.is_not_set("email") # property is null ``` **Boolean filters:** ``` Filter.is_true("is_premium") # boolean true Filter.is_false("is_trial") # boolean false ``` ### List-of-object filters When a property's value is a list of objects (e.g. `cart` is a list of `{Brand, Category, Price}` items), use `Filter.list_contains` to filter on a subproperty. Discover valid subproperty names and types via [`Workspace.subproperties()`](https://mixpanel.github.io/mixpanel-headless/guide/discovery/#subproperties) first. **Keyword shorthand** for the common equality case β€” each `key=value` becomes an inner equality filter. All inner conditions must match the same item: ``` # Cart contains a nike-branded hat result = ws.query( "Cart Viewed", where=Filter.list_contains("cart", Brand="nike", Category="hats"), ) ``` **Explicit `Filter` instances** for any non-equality operator: ``` # Cart contains an item costing more than $50 result = ws.query( "Cart Viewed", where=Filter.list_contains("cart", Filter.greater_than("Price", 50)), ) ``` **Quantifier** β€” `"any"` (default) requires at least one item to satisfy all inner conditions; `"all"` requires every item to: ``` # Every cart item costs more than $50 where = Filter.list_contains( "cart", Filter.greater_than("Price", 50), quantifier="all", ) ``` **Resource type** β€” `Filter.list_contains` accepts `resource_type="people"` for list-of-object people properties (e.g. `addresses`). When using kwarg shorthand, the inner equality filters inherit the outer `resource_type`. When passing positional `Filter` instances, each carries its own `resource_type` from its own factory call β€” pass `resource_type=` explicitly on each inner factory if you want them to match the outer. ``` # Filter people by a list-of-object property where = Filter.list_contains("addresses", resource_type="people", City="Brooklyn") ``` Cannot be nested (a `list_contains` cannot appear inside another `list_contains`). Mixing the kwarg and positional shapes in one call raises `ValueError`. ### Per-Metric Filters Apply filters to individual metrics using `Metric.filters`: ``` from mixpanel_headless import Metric, Filter # Different filters on each event result = ws.query([ Metric("Purchase", math="unique"), Metric( "Purchase", math="unique", filters=[Filter.equals("plan", "premium")], ), ]) ``` By default, multiple per-metric filters combine with AND logic. Use `filters_combinator="any"` for OR logic: ``` result = ws.query(Metric( "Purchase", math="unique", filters=[ Filter.equals("country", "US"), Filter.equals("country", "CA"), ], filters_combinator="any", # match US OR CA )) ``` ### Date Filters Filter by datetime properties using purpose-built factory methods: ``` from mixpanel_headless import Filter # Absolute date filters Filter.on("created", "2025-01-15") # exact date match Filter.not_on("created", "2025-01-15") # not on date Filter.before("created", "2025-01-01") # before a date Filter.since("created", "2025-01-01") # on or after a date Filter.date_between("created", "2025-01-01", "2025-06-30") # date range # Relative date filters β€” "in the last N units" Filter.in_the_last("created", 30, "day") # last 30 days Filter.in_the_last("last_seen", 2, "week") # last 2 weeks Filter.not_in_the_last("created", 90, "day") # NOT in last 90 days Filter.date_not_between("created", "2025-01-01", "2025-06-30") # dates outside a range Filter.in_the_next("renewal_date", 30, "day") # relative future date ``` The relative date methods accept a `FilterDateUnit`: `"hour"`, `"day"`, `"week"`, or `"month"`. ``` from mixpanel_headless import FilterDateUnit # Literal["hour", "day", "week", "month"] # Example: recent signups with purchases result = ws.query( "Purchase", where=Filter.in_the_last("signup_date", 7, "day"), last=30, ) ``` ## Breakdowns ### String Breakdowns Break down results by property values with `group_by`: ``` # Simple string breakdown result = ws.query("Login", group_by="platform", last=14) # Multiple breakdowns result = ws.query("Purchase", group_by=["country", "platform"]) ``` ### The `GroupBy` Class For numeric bucketing, boolean breakdowns, or explicit type annotations, use `GroupBy`: ``` from mixpanel_headless import GroupBy # Numeric breakdown with buckets result = ws.query( "Purchase", group_by=GroupBy( "revenue", property_type="number", bucket_size=50, bucket_min=0, bucket_max=500, ), ) # Boolean breakdown result = ws.query( "Login", group_by=GroupBy("is_premium", property_type="boolean"), ) # Mixed: string shorthand + GroupBy result = ws.query( "Purchase", group_by=[ "country", GroupBy("amount", property_type="number", bucket_size=25), ], ) ``` ### List-of-object breakdowns Mirror `Filter.list_contains` for breakdowns: when a property is a list of objects, break down by one of its subproperties via `GroupBy.list_item`. Discover valid subproperty names and types via [`Workspace.subproperties()`](https://mixpanel.github.io/mixpanel-headless/guide/discovery/#subproperties). ``` from mixpanel_headless import GroupBy # Break down Cart Viewed events by cart.Brand result = ws.query("Cart Viewed", group_by=GroupBy.list_item("cart", "Brand")) # Break down by a numeric subproperty (sub_type controls aggregation) result = ws.query( "Cart Viewed", group_by=GroupBy.list_item("cart", "Price", sub_type="number"), ) # Mix list-item with regular breakdowns result = ws.query( "Cart Viewed", group_by=["country", GroupBy.list_item("cart", "Brand")], ) ``` `sub_type` accepts the four scalar values from `CustomPropertyType` (`"string"`, `"number"`, `"boolean"`, `"datetime"`). Bucketing (`bucket_size`/`bucket_min`/`bucket_max`) is incompatible with list-item breakdowns. Asymmetric with `Filter.list_contains` `GroupBy.list_item` is **events-only** β€” there is no `resource_type` parameter, because Mixpanel's UI does not support list-of-object breakdowns for people properties. `Filter.list_contains` accepts `resource_type="people"` because the wire format permits list-object filters on people properties (just not breakdowns). ## Formulas Compute derived metrics from multiple events. Letters A-Z reference events by their position in the list. ### Top-Level `formula` Parameter ``` from mixpanel_headless import Metric # Conversion rate: purchases / signups * 100 result = ws.query( [Metric("Signup", math="unique"), Metric("Purchase", math="unique")], formula="(B / A) * 100", formula_label="Conversion Rate", unit="week", ) ``` When `formula` is set, the underlying metrics are automatically hidden β€” only the formula result appears in the output. ### `Formula` Class in Events List For inline formula definitions, pass `Formula` objects alongside events: ``` from mixpanel_headless import Metric, Formula result = ws.query([ Metric("Signup", math="unique"), Metric("Purchase", math="unique"), Formula("(B / A) * 100", label="Conversion Rate"), ]) ``` Both approaches produce identical results. Use whichever reads more naturally. ### Multi-Metric Comparison (No Formula) Compare multiple events side by side without a formula: ``` # Three events on the same chart result = ws.query( ["Signup", "Login", "Purchase"], math="unique", last=30, ) ``` ## Time Ranges ### Relative (Default) By default, `query()` returns the last 30 days. Customize with `last` and `unit`: ``` # Last 7 days (daily granularity) result = ws.query("Login", last=7) # Last 4 weeks (weekly granularity) result = ws.query("Login", last=4, unit="week") # Last 6 months result = ws.query("Login", last=6, unit="month") ``` The `unit` controls both what "last N" means and how data is bucketed on the time axis. ### Absolute Specify explicit start and end dates: ``` # Q1 2025 result = ws.query( "Purchase", from_date="2025-01-01", to_date="2025-03-31", unit="week", ) # From a date to today result = ws.query("Login", from_date="2025-01-01") ``` Dates must be in `YYYY-MM-DD` format. ### Hourly Granularity Use `unit="hour"` for intraday analysis: ``` result = ws.query("Login", last=2, unit="hour") ``` ## Analysis Modes ### Rolling Windows Smooth noisy data with a rolling average: ``` # 7-day rolling average of signups by country result = ws.query( "Signup", math="unique", group_by="country", rolling=7, last=60, ) ``` ### Cumulative Show running totals over time: ``` result = ws.query("Signup", math="unique", cumulative=True, last=30) ``` Note `rolling` and `cumulative` are mutually exclusive. ### Result Modes The `mode` parameter controls result aggregation semantics: | Mode | Semantics | Use case | | ------------------------ | -------------------------------------- | ------------------- | | `"timeseries"` (default) | Per-period values | Trends over time | | `"total"` | Single aggregate across the date range | KPI numbers | | `"table"` | Tabular detail | Detailed breakdowns | ``` # Single KPI number: total unique purchasers this month result = ws.query( "Purchase", math="unique", from_date="2025-03-01", to_date="2025-03-31", mode="total", ) total = result.df["count"].iloc[0] ``` Mode affects aggregation `mode="total"` with `math="unique"` deduplicates users across the **entire date range**. `mode="timeseries"` with `math="unique"` counts unique users **per period** (not additive across periods). This is not just a display difference β€” it changes the numbers. ## Period-over-Period Comparison Compare the current time range against a previous period using `TimeComparison`: ``` from mixpanel_headless import TimeComparison # Compare against previous week result = ws.query("Login", time_comparison=TimeComparison.relative("week"), last=7) # Compare against window starting on a fixed date result = ws.query( "Purchase", time_comparison=TimeComparison.absolute_start("2025-01-01"), from_date="2026-01-01", to_date="2026-01-31", ) # Compare against window ending on a fixed date result = ws.query( "Purchase", time_comparison=TimeComparison.absolute_end("2025-12-31"), from_date="2026-01-01", to_date="2026-01-31", ) ``` Three factory methods: | Method | What it compares against | | ------------------------------------- | ---------------------------------------------------------------- | | `TimeComparison.relative(unit)` | Previous period offset by unit (day, week, month, quarter, year) | | `TimeComparison.absolute_start(date)` | Window starting on a fixed date, same duration | | `TimeComparison.absolute_end(date)` | Window ending on a fixed date, same duration | `TimeComparison` also works with `query_funnel()` and `query_retention()`. ## Frequency Analysis ### Frequency Breakdown Break down results by how often users performed an event using `FrequencyBreakdown`: ``` from mixpanel_headless import FrequencyBreakdown # How are logins distributed by purchase frequency? result = ws.query( "Login", group_by=FrequencyBreakdown( event="Purchase", bucket_size=1, bucket_min=0, bucket_max=10, ), last=30, ) ``` Parameters: | Parameter | Type | Default | Description | | ------------- | ------------- | -------- | ------------------------------ | | `event` | `str` | required | Event to count frequency for | | `bucket_size` | `int` | `1` | Width of each frequency bucket | | `bucket_min` | `int` | `0` | Minimum frequency value | | `bucket_max` | `int` | `10` | Maximum frequency value | | `label` | `str \| None` | `None` | Display label | ### Frequency Filter Filter to users who performed an event a certain number of times using `FrequencyFilter`: ``` from mixpanel_headless import FrequencyFilter # Only users who purchased at least 3 times result = ws.query( "Login", where=FrequencyFilter( event="Purchase", value=3, operator="is at least", ), last=30, ) # With a lookback window β€” purchased at least 3 times in the last 30 days result = ws.query( "Login", where=FrequencyFilter( event="Purchase", value=3, operator="is at least", date_range_value=30, date_range_unit="day", ), last=90, ) ``` Operators: `"is at least"`, `"is at most"`, `"is greater than"`, `"is less than"`, `"is equal to"`. ## Data Groups Scope a query to a specific data group for group-level analytics: ``` result = ws.query("Login", data_group_id=42, last=30) ``` `data_group_id` is available on all query engines: `query()`, `query_funnel()`, `query_retention()`, and `query_flow()`. ## Working with Results ### `QueryResult` `query()` returns a `QueryResult` with: ``` result = ws.query("Login", math="unique", last=7) # DataFrame (lazy, cached) result.df # pandas DataFrame # Raw series data result.series # {"Login [Unique Users]": {"2025-03-01...": 142, ...}} # Time range result.from_date # "2025-03-25T00:00:00-07:00" result.to_date # "2025-03-31T23:59:59.999000-07:00" # Metadata result.computed_at # "2025-03-31T12:00:00.000000+00:00" result.headers # ["$metric"] result.meta # {"min_sampling_factor": 1.0, ...} # Generated bookmark params (for debugging or persistence) result.params # dict β€” the full bookmark JSON sent to API ``` ### DataFrame Structure For **timeseries** mode, the DataFrame has columns `date`, `event`, `count`: ``` result = ws.query(["Signup", "Login"], math="unique") print(result.df.head()) # date event count # 0 2025-03-01 Signup [Unique Users] 85 # 1 2025-03-01 Login [Unique Users] 312 # 2 2025-03-02 Signup [Unique Users] 92 ``` For **total** mode, the DataFrame has columns `event`, `count` (no date). ### Persisting as a Saved Report The generated bookmark params can be saved as a Mixpanel report: ``` from mixpanel_headless import CreateBookmarkParams # Run query result = ws.query("Login", math="dau", group_by="platform", last=90) # Save as a report using the generated params ws.create_bookmark(CreateBookmarkParams( name="DAU by Platform (90d)", bookmark_type="insights", params=result.params, )) ``` ### Debugging Inspect `result.params` to see the exact bookmark JSON sent to the API. This is useful for: - Understanding what was actually queried - Comparing with Mixpanel web UI bookmark params - Diagnosing unexpected results ``` import json result = ws.query("Login", math="unique", group_by="platform") print(json.dumps(result.params, indent=2)) ``` ## Validation `query()` validates all parameter combinations **before** making an API call and raises `ValueError` with descriptive messages: | Rule | Error message | | ------------------------------- | ------------------------------------------------------------ | | Property math without property | `math='average' requires math_property to be set` | | Property set with counting math | `math_property is only valid with property-based math types` | | Per-user with DAU/WAU/MAU | `per_user is incompatible with math='dau'` | | Formula with < 2 events | `formula requires at least 2 events` | | Rolling + cumulative | `rolling and cumulative are mutually exclusive` | | `to_date` without `from_date` | `to_date requires from_date` | | Invalid date format | `from_date must be YYYY-MM-DD format` | | Invalid bucket config | `bucket_min/bucket_max require bucket_size` | ## Complete Examples ### Revenue Dashboard Metrics ``` import mixpanel_headless as mp from mixpanel_headless import Metric, Filter, GroupBy ws = mp.Workspace() # Total revenue by country this quarter revenue = ws.query( "Purchase", math="total", math_property="amount", group_by="country", from_date="2025-01-01", to_date="2025-03-31", unit="month", ) # Revenue distribution by bucket distribution = ws.query( "Purchase", group_by=GroupBy( "amount", property_type="number", bucket_size=25, bucket_min=0, bucket_max=500, ), last=30, ) # Conversion rate with per-metric filters conversion = ws.query( [ Metric("Purchase", math="unique"), Metric( "Purchase", math="unique", filters=[Filter.equals("plan", "premium")], ), ], formula="(B / A) * 100", formula_label="Premium %", group_by="platform", unit="week", ) ``` ### User Engagement Analysis ``` # 7-day rolling average of DAU by platform engagement = ws.query( "Login", math="dau", group_by="platform", rolling=7, last=90, ) # Average sessions per user per week sessions = ws.query( "Session Start", math="total", per_user="average", unit="week", last=12, ) # WAU trend for premium users wau = ws.query( "Login", math="wau", where=Filter.is_true("is_premium"), last=6, unit="month", ) ``` ## Generating Params Without Querying Use `build_params()` to generate bookmark params without making an API call β€” useful for debugging, inspecting the generated JSON, or saving queries as reports: ``` # Same arguments as query(), returns dict instead of QueryResult params = ws.build_params( "Login", math="dau", group_by="platform", where=Filter.in_the_last("created", 30, "day"), last=90, ) import json print(json.dumps(params, indent=2)) # inspect the generated bookmark JSON # Save as a report directly from params ws.create_bookmark(CreateBookmarkParams( name="DAU by Platform (90d)", bookmark_type="insights", params=params, )) ``` ## What's Next `query()` is the foundation for a family of typed query methods. Each follows the same pattern β€” typed Python arguments generating the correct bookmark params: - **[`query_funnel()`](https://mixpanel.github.io/mixpanel-headless/guide/query-funnels/index.md)** β€” Ad-hoc funnel conversion analysis with typed step definitions, exclusions, and conversion windows - **[`query_retention()`](https://mixpanel.github.io/mixpanel-headless/guide/query-retention/index.md)** β€” Ad-hoc retention curves with event pairs, custom buckets, and alignment modes - **[`query_flow()`](https://mixpanel.github.io/mixpanel-headless/guide/query-flows/index.md)** β€” Ad-hoc flow path analysis with step definitions, direction controls, and visualization modes - **Cohort-scoped queries** β€” Filter, break down, or track cohort membership across all engines (see below) ## Cohort-Scoped Queries Scope any query to a user segment β€” filter by cohort membership, break down by cohort, or track cohort size as a metric. Use saved cohort IDs or define cohorts inline with `CohortDefinition`. ### Cohort Filters Restrict queries to users in (or not in) a cohort using `Filter.in_cohort()` and `Filter.not_in_cohort()`: ``` from mixpanel_headless import Filter, CohortCriteria, CohortDefinition # Saved cohort result = ws.query("Purchase", where=Filter.in_cohort(123, "Power Users")) # Inline cohort β€” define the segment right where you use it power_users = CohortDefinition( CohortCriteria.did_event("Purchase", at_least=3, within_days=30) ) result = ws.query("Login", where=Filter.in_cohort(power_users, name="Power Users")) # Exclude a cohort result = ws.query("Purchase", where=Filter.not_in_cohort(789, "Bots")) # Combine with property filters result = ws.query( "Purchase", where=[Filter.in_cohort(power_users, name="PU"), Filter.equals("platform", "iOS")], ) ``` Cohort filters work with all five query methods: `query()`, `query_funnel()`, `query_retention()`, `query_flow()`, and `query_user()`. ### Cohort Breakdowns Segment results by cohort membership using `CohortBreakdown` in the `group_by=` parameter: ``` from mixpanel_headless import CohortBreakdown # Compare cohort vs. everyone else result = ws.query( "Purchase", group_by=CohortBreakdown(123, "Power Users"), ) # Result segments: "Power Users" and "Not In Power Users" # Inline cohort breakdown result = ws.query( "Purchase", group_by=CohortBreakdown(power_users, name="Power Users"), ) # Only the cohort segment (no "Not In" group) result = ws.query( "Purchase", group_by=CohortBreakdown(123, "PU", include_negated=False), ) # Mix with property breakdowns result = ws.query( "Purchase", group_by=[CohortBreakdown(123, "Power Users"), "platform"], ) ``` Cohort breakdowns work with `query()`, `query_funnel()`, and `query_retention()` (not flows). ### Cohort Metrics Track cohort size over time as a metric β€” insights only: ``` from mixpanel_headless import CohortMetric, Metric # Track cohort growth result = ws.query(CohortMetric(123, "Power Users"), last=90, unit="week") # What % of active users are power users? result = ws.query( [Metric("Login", math="unique"), CohortMetric(123, "Power Users")], formula="(B / A) * 100", formula_label="Power User %", ) ``` `CohortMetric` is insights-only β€” it cannot be used with `query_funnel()`, `query_retention()`, or `query_flow()`. ### Engine Compatibility | Capability | `query()` | `query_funnel()` | `query_retention()` | `query_flow()` | | ----------------------------------- | --------- | ---------------- | ------------------- | -------------- | | **Cohort Filters** (`where=`) | βœ“ | βœ“ | βœ“ | βœ“ | | **Cohort Breakdowns** (`group_by=`) | βœ“ | βœ“ | βœ“ | β€” | | **Cohort Metrics** (`events=`) | βœ“ | β€” | β€” | β€” | ## Custom Properties in Queries Use saved custom properties or define computed properties inline β€” in breakdowns, filters, and metric measurement. Custom properties work everywhere a plain string property name does. To create and manage custom properties in Mixpanel, see [Data Governance β€” Custom Properties](https://mixpanel.github.io/mixpanel-headless/guide/data-governance/#custom-properties). ### Referencing a Saved Custom Property Use `CustomPropertyRef` to reference a custom property that already exists in your Mixpanel project by its numeric ID: ``` from mixpanel_headless import CustomPropertyRef, GroupBy, Filter, Metric ref = CustomPropertyRef(42) # Breakdown by saved custom property result = ws.query("Purchase", group_by=GroupBy(property=ref, property_type="number")) # Filter by saved custom property result = ws.query("Purchase", where=Filter.greater_than(property=ref, value=100)) # Aggregate a saved custom property result = ws.query(Metric("Purchase", math="average", property=ref)) ``` Find custom property IDs with `ws.list_custom_properties()` or `mp custom-properties list`. ### Inline Custom Properties Use `InlineCustomProperty` to define a computed property at query time β€” no need to save it to your project first. Formulas reference raw properties through single-letter variables (A–Z), each mapped to a `PropertyInput`: ``` from mixpanel_headless import InlineCustomProperty, PropertyInput # Full constructor β€” explicit control over types revenue = InlineCustomProperty( formula="A * B", inputs={ "A": PropertyInput("price", type="number"), "B": PropertyInput("quantity", type="number"), }, property_type="number", ) ``` For the common case of numeric formulas over event properties, use the `numeric()` convenience constructor: ``` # Shorthand β€” auto-creates numeric PropertyInput objects revenue = InlineCustomProperty.numeric("A * B", A="price", B="quantity") ``` Both forms produce identical results. Use the full constructor when you need non-numeric types or user-profile properties (`resource_type="user"`). ### Custom Property Breakdowns Pass a custom property to `GroupBy.property` for breakdowns. Numeric bucketing works the same as with regular properties: ``` from mixpanel_headless import GroupBy, CustomPropertyRef, InlineCustomProperty # Saved custom property with numeric buckets result = ws.query( "Purchase", group_by=GroupBy( property=CustomPropertyRef(42), property_type="number", bucket_size=50, ), ) # Inline computed property result = ws.query( "Purchase", group_by=GroupBy( property=InlineCustomProperty.numeric("A * B", A="price", B="quantity"), property_type="number", bucket_size=100, bucket_min=0, bucket_max=1000, ), ) # Mix with regular property breakdowns result = ws.query( "Purchase", group_by=["country", GroupBy(property=CustomPropertyRef(42), property_type="number")], ) ``` ### Custom Property Filters All 18 `Filter` factory methods accept custom properties in the `property` parameter: ``` from mixpanel_headless import Filter, CustomPropertyRef, InlineCustomProperty # Saved custom property result = ws.query( "Purchase", where=Filter.greater_than(property=CustomPropertyRef(42), value=100), ) # Inline computed property result = ws.query( "Purchase", where=Filter.between( property=InlineCustomProperty.numeric("A * B", A="price", B="quantity"), value=[100, 1000], ), ) # Combine with regular filters result = ws.query( "Purchase", where=[ Filter.equals("country", "US"), Filter.greater_than(property=CustomPropertyRef(42), value=50), ], ) ``` ### Custom Property Measurement Aggregate a custom property as the metric value using `Metric(property=...)`: ``` from mixpanel_headless import Metric, CustomPropertyRef, InlineCustomProperty # Average of a saved custom property result = ws.query( Metric("Purchase", math="average", property=CustomPropertyRef(42)), ) # Sum of an inline computed property result = ws.query( Metric("Purchase", math="total", property=InlineCustomProperty.numeric("A * B", A="price", B="quantity")), ) # Per-metric custom properties in multi-metric queries result = ws.query([ Metric("Purchase", math="total", property=InlineCustomProperty.numeric("A * B", A="price", B="quantity")), Metric("Purchase", math="unique"), ]) ``` Use `Metric(property=...)`, not `math_property=` The top-level `math_property` parameter only accepts plain string property names. To use a custom property for measurement, wrap the event in a `Metric` object and set `property=` on it. ### Engine Compatibility | Capability | `query()` | `query_funnel()` | `query_retention()` | `query_flow()` | | --------------------------------------- | --------- | ---------------- | ------------------- | -------------- | | **CP Breakdowns** (`group_by=`) | βœ“ | βœ“ | βœ“ | β€” | | **CP Filters** (`where=`) | βœ“ | ⚠ | ⚠ | β€” | | **CP Measurement** (`Metric.property=`) | βœ“ | β€” | β€” | β€” | ⚠ = Supported, but a known Mixpanel server bug may cause errors when custom property filters are used in funnel and retention global `where=`. Custom property breakdowns and measurement work reliably in those engines. `query_flow()` does not support custom properties in any position (Mixpanel limitation). ### Custom Property Validation Custom properties are validated **before** any API call. Invalid configurations raise `BookmarkValidationError`: | Rule | Error code | Error message | | ----------------------------- | ----------------------- | --------------------------------------------------------------- | | ID must be positive integer | `CP1_INVALID_ID` | custom property ID must be a positive integer (got {id}) | | Formula must be non-empty | `CP2_EMPTY_FORMULA` | inline custom property formula must be non-empty | | At least one input required | `CP3_EMPTY_INPUTS` | inline custom property must have at least one input | | Input keys must be single A–Z | `CP4_INVALID_INPUT_KEY` | input keys must be single uppercase letters (A-Z), got {key!r} | | Formula max 20,000 chars | `CP5_FORMULA_TOO_LONG` | formula exceeds maximum length of 20,000 characters (got {len}) | | Input property name non-empty | `CP6_EMPTY_INPUT_NAME` | input {key!r} has an empty property name | ``` from mixpanel_headless import BookmarkValidationError, CustomPropertyRef try: ws.query("Purchase", group_by=GroupBy(property=CustomPropertyRef(0), property_type="number")) except BookmarkValidationError as e: for error in e.errors: print(f"[{error.code}] {error.path}: {error.message}") # [CP1_INVALID_ID] group_by[0].property: custom property ID must be a positive integer (got 0) ``` ## Next Steps - [Funnel Queries](https://mixpanel.github.io/mixpanel-headless/guide/query-funnels/index.md) β€” Typed funnel conversion analysis - [Retention Queries](https://mixpanel.github.io/mixpanel-headless/guide/query-retention/index.md) β€” Typed retention analysis with event pairs and custom buckets - [Flow Queries](https://mixpanel.github.io/mixpanel-headless/guide/query-flows/index.md) β€” Typed flow path analysis with steps, directions, and graph output - [Live Analytics](https://mixpanel.github.io/mixpanel-headless/guide/live-analytics/index.md) β€” Legacy query methods (segmentation, funnels, retention) - [Data Discovery](https://mixpanel.github.io/mixpanel-headless/guide/discovery/index.md) β€” Explore events and properties before querying - [Data Governance β€” Custom Properties](https://mixpanel.github.io/mixpanel-headless/guide/data-governance/#custom-properties) β€” Create and manage custom properties - [API Reference β€” Workspace](https://mixpanel.github.io/mixpanel-headless/api/workspace/index.md) β€” Full method signatures - [API Reference β€” Types](https://mixpanel.github.io/mixpanel-headless/api/types/index.md) β€” Metric, Filter, GroupBy, CustomPropertyRef, InlineCustomProperty, QueryResult details Copy markdown # Funnel Queries Build typed funnel conversion analysis against Mixpanel's Insights engine β€” define steps, exclusions, and conversion windows inline without creating saved funnels first. Recommended `Workspace.query_funnel()` is the typed way to run funnel analysis programmatically. It supports capabilities not available through the legacy `funnel()` method, including ad-hoc step definitions, per-step filters, exclusions, holding constant properties, and session-based conversion. ## When to Use `query_funnel()` `query_funnel()` builds funnel bookmark params and posts them to the Insights engine. The legacy `funnel()` method queries pre-saved funnels by ID. Use `query_funnel()` when you need any of the capabilities in the right column: | Capability | Legacy `funnel()` | `query_funnel()` | | -------------------------- | ----------------------- | --------------------------------------------------- | | Query a saved funnel by ID | `funnel(funnel_id=123)` | Use `query_saved_report()` | | Define steps inline | Not available | `["Signup", "Purchase"]` | | Per-step filters | Not available | `FunnelStep("Purchase", filters=[...])` | | Per-step labels | Not available | `FunnelStep("Purchase", label="High-Value")` | | Exclusions between steps | Not available | `exclusions=["Logout"]` | | Hold properties constant | Not available | `holding_constant=["platform"]` | | Conversion window control | Not available | `conversion_window=7, conversion_window_unit="day"` | | Session-based conversion | Not available | `conversion_window_unit="session"` | | Step ordering modes | Not available | `order="any"` | | Property breakdowns | `on="country"` | `group_by="country"` | | Save query as a report | N/A | `result.params` β†’ `create_bookmark()` | Use the legacy `funnel()` when: - You have an existing saved funnel in Mixpanel and just need its results β†’ `funnel(funnel_id=123)` - You need simple segmentation of a saved funnel β†’ `funnel(funnel_id=123, on="country")` ## Getting Started The simplest possible funnel β€” two steps with default settings (14-day conversion window, last 30 days): ``` import mixpanel_headless as mp ws = mp.Workspace() result = ws.query_funnel(["Signup", "Purchase"]) print(f"Conversion: {result.overall_conversion_rate:.1%}") # Conversion: 12.3% print(result.df) # step event count step_conv_ratio overall_conv_ratio avg_time avg_time_from_start # 1 1 Signup 1000 1.00 1.00 0.0 0.0 # 2 2 Purchase 123 0.12 0.12 3600.0 3600.0 ``` Add a conversion window and time range: ``` # 7-day conversion window over the last 90 days result = ws.query_funnel( ["Signup", "Add to Cart", "Checkout", "Purchase"], conversion_window=7, last=90, ) ``` ## Steps ### Plain Strings The simplest way to define steps β€” pass event names as strings: ``` result = ws.query_funnel(["Signup", "Add to Cart", "Purchase"]) ``` At least 2 steps are required, up to a maximum of 100. ### The `FunnelStep` Class For per-step configuration, use `FunnelStep` objects: ``` from mixpanel_headless import FunnelStep, Filter result = ws.query_funnel([ FunnelStep("Signup"), FunnelStep( "Purchase", label="High-Value Purchase", filters=[Filter.greater_than("amount", 50)], ), ]) ``` `FunnelStep` fields: | Field | Type | Default | Description | | -------------------- | -------------------------- | ---------- | -------------------------------------- | | `event` | `str` | (required) | Mixpanel event name | | `label` | `str \| None` | `None` | Display label (defaults to event name) | | `filters` | `list[Filter] \| None` | `None` | Per-step filter conditions | | `filters_combinator` | `"all" \| "any"` | `"all"` | How per-step filters combine (AND/OR) | | `order` | `"loose" \| "any" \| None` | `None` | Per-step ordering override | Plain strings and `FunnelStep` objects can be mixed freely: ``` result = ws.query_funnel([ "Signup", # plain string β€” no filters needed FunnelStep("Purchase", filters=[Filter.greater_than("amount", 50)]), ]) ``` ### Per-Step Filters Apply filters to individual steps using `FunnelStep.filters`. These restrict which events count for that specific step: ``` from mixpanel_headless import FunnelStep, Filter result = ws.query_funnel([ FunnelStep("Signup", filters=[Filter.equals("source", "organic")]), FunnelStep("Purchase", filters=[ Filter.equals("country", "US"), Filter.greater_than("amount", 25), ]), ]) ``` By default, multiple per-step filters combine with AND logic. Use `filters_combinator="any"` for OR logic: ``` result = ws.query_funnel([ "Signup", FunnelStep( "Purchase", filters=[ Filter.equals("country", "US"), Filter.equals("country", "CA"), ], filters_combinator="any", # match US OR CA ), ]) ``` See [Insights Queries β€” Filters](https://mixpanel.github.io/mixpanel-headless/guide/query/#filters) for the full list of `Filter` factory methods. ## Conversion Window Control how long users have to complete the funnel: ``` # 7 days (default unit is "day") result = ws.query_funnel(["Signup", "Purchase"], conversion_window=7) # 2 hours result = ws.query_funnel(["View", "Click"], conversion_window=2, conversion_window_unit="hour") # 4 weeks result = ws.query_funnel(["Signup", "Purchase"], conversion_window=4, conversion_window_unit="week") ``` ### Conversion Window Units | Unit | Max value | Description | | ----------------- | ---------- | ---------------- | | `"second"` | 31,708,800 | Seconds (min: 2) | | `"minute"` | 528,480 | Minutes | | `"hour"` | 8,808 | Hours | | `"day"` (default) | 367 | Days | | `"week"` | 52 | Weeks | | `"month"` | 12 | Months | | `"session"` | 12 | Sessions | All maximum values correspond to approximately 366 days. ### Session-Based Conversion Use `conversion_window_unit="session"` to count conversions within a single Mixpanel session: ``` # Users must complete all steps within one session result = ws.query_funnel( ["View Product", "Add to Cart", "Purchase"], conversion_window=1, conversion_window_unit="session", math="conversion_rate_session", ) ``` Note Session-based conversion requires `conversion_window=1` and `math="conversion_rate_session"`. ## Ordering The `order` parameter controls how steps must be completed: | Order | Behavior | | ------------------- | --------------------------------------------------------------------------------- | | `"loose"` (default) | Steps must happen in the specified order, but other events can occur between them | | `"any"` | Steps can happen in any order within the conversion window | ``` # Steps in any order result = ws.query_funnel( ["Feature A Used", "Feature B Used", "Feature C Used"], order="any", conversion_window=30, ) ``` ### Per-Step Order Override When the top-level `order` is `"any"`, individual steps can override to `"loose"`: ``` result = ws.query_funnel( [ FunnelStep("Signup"), # must come first (loose) FunnelStep("Feature A", order="any"), FunnelStep("Feature B", order="any"), ], order="any", ) ``` ## Aggregation The `math` parameter controls what metric is computed. Default: `"conversion_rate_unique"`. ### Conversion Rates | Math type | What it measures | | ------------------------------------ | ----------------------------- | | `"conversion_rate_unique"` (default) | Unique-user conversion rate | | `"conversion_rate_total"` | Total-event conversion rate | | `"conversion_rate_session"` | Session-based conversion rate | ``` # Total-event conversion (counts all events, not just unique users) result = ws.query_funnel( ["View", "Purchase"], math="conversion_rate_total", ) ``` ### Raw Counts | Math type | What it counts | | ---------- | -------------------------- | | `"unique"` | Unique users per step | | `"total"` | Total event count per step | ``` # Raw unique user counts at each step result = ws.query_funnel(["Signup", "Purchase"], math="unique") ``` ### Property Aggregation | Math type | Aggregation | | ------------------------------------- | ------------------------------------------- | | `"average"` | Mean of a numeric property per step | | `"median"` | Median value | | `"min"` / `"max"` | Extremes | | `"p25"` / `"p75"` / `"p90"` / `"p99"` | Percentiles | | `"histogram"` | Distribution of a numeric property per step | ``` # Average purchase amount at each step result = ws.query_funnel( ["Signup", "Purchase"], math="average", math_property="amount", ) # Distribution of purchase amounts at each funnel step result = ws.query_funnel( ["Browse", "Add to Cart", "Purchase"], math="histogram", math_property="amount", ) ``` ## Filters ### Global Filters Apply filters across all steps with `where=`: ``` from mixpanel_headless import Filter # Filter the entire funnel result = ws.query_funnel( ["Signup", "Purchase"], where=Filter.equals("country", "US"), ) # Multiple global filters (AND logic) result = ws.query_funnel( ["Signup", "Purchase"], where=[ Filter.equals("platform", "web"), Filter.is_true("is_premium"), ], ) ``` Global filters apply to all steps in the funnel. For step-specific filtering, use `FunnelStep.filters` (see [Steps β€” Per-Step Filters](#per-step-filters)). See [Insights Queries β€” Available Filter Methods](https://mixpanel.github.io/mixpanel-headless/guide/query/#available-filter-methods) for the complete filter reference. ### Cohort Filters Restrict the funnel to users in a cohort β€” saved or inline: ``` from mixpanel_headless import Filter, CohortCriteria, CohortDefinition # Saved cohort result = ws.query_funnel( ["Signup", "Purchase"], where=Filter.in_cohort(123, "Power Users"), ) # Inline cohort β€” no pre-saved cohort needed active_users = CohortDefinition( CohortCriteria.did_event("Login", at_least=5, within_days=7) ) result = ws.query_funnel( ["Signup", "Purchase"], where=Filter.in_cohort(active_users, name="Active Users"), ) ``` See [Insights Queries β€” Cohort Filters](https://mixpanel.github.io/mixpanel-headless/guide/query/#cohort-filters) for the full cohort filter reference. ### Custom Property Filters Use saved or inline custom properties in funnel filters: ``` from mixpanel_headless import Filter, CustomPropertyRef result = ws.query_funnel( ["Signup", "Purchase"], where=Filter.greater_than(property=CustomPropertyRef(42), value=100), ) ``` Warning Custom property filters in funnel `where=` may cause server errors due to a known Mixpanel API bug. Custom property breakdowns and measurement work reliably. See [Insights Queries β€” Custom Properties in Queries](https://mixpanel.github.io/mixpanel-headless/guide/query/#custom-properties-in-queries) for `InlineCustomProperty`, validation rules, and full options. ## Breakdowns Break down funnel results by property values with `group_by`: ``` from mixpanel_headless import GroupBy # Simple string breakdown result = ws.query_funnel(["Signup", "Purchase"], group_by="platform") # Multiple breakdowns result = ws.query_funnel(["Signup", "Purchase"], group_by=["country", "platform"]) # Numeric bucketing result = ws.query_funnel( ["Signup", "Purchase"], group_by=GroupBy("amount", property_type="number", bucket_size=50), ) ``` ### Cohort Breakdowns Segment funnel results by cohort membership: ``` from mixpanel_headless import CohortBreakdown # Compare power users vs. everyone else through the funnel result = ws.query_funnel( ["Signup", "Purchase"], group_by=CohortBreakdown(123, "Power Users"), ) ``` See [Insights Queries β€” Cohort Breakdowns](https://mixpanel.github.io/mixpanel-headless/guide/query/#cohort-breakdowns) for inline definitions and options. ### Custom Property Breakdowns Break down funnel results by a saved or inline custom property: ``` from mixpanel_headless import GroupBy, InlineCustomProperty result = ws.query_funnel( ["Signup", "Purchase"], group_by=GroupBy( property=InlineCustomProperty.numeric("A * B", A="price", B="quantity"), property_type="number", bucket_size=50, ), ) ``` See [Insights Queries β€” Custom Properties in Queries](https://mixpanel.github.io/mixpanel-headless/guide/query/#custom-properties-in-queries) for `CustomPropertyRef` and full options. See [Insights Queries β€” Breakdowns](https://mixpanel.github.io/mixpanel-headless/guide/query/#breakdowns) for the full `GroupBy` reference. ## Exclusions Exclude users who perform specific events between funnel steps. Users who trigger an excluded event within the specified step range are removed from the funnel. ### String Shorthand Pass event names as strings to exclude them between all steps: ``` # Exclude users who log out anywhere in the funnel result = ws.query_funnel( ["Signup", "Add to Cart", "Purchase"], exclusions=["Logout"], ) ``` ### The `Exclusion` Class For targeted exclusion between specific steps, use `Exclusion` objects: ``` from mixpanel_headless import Exclusion result = ws.query_funnel( ["Signup", "Add to Cart", "Checkout", "Purchase"], exclusions=[ Exclusion("Logout"), # between all steps (same as string) Exclusion("Refund", from_step=2, to_step=3), # only between Checkout and Purchase ], ) ``` `Exclusion` fields: | Field | Type | Default | Description | | ----------- | ------------- | ---------- | ----------------------------------------------------------------- | | `event` | `str` | (required) | Event name to exclude | | `from_step` | `int` | `0` | Start of exclusion range (0-indexed, inclusive) | | `to_step` | `int \| None` | `None` | End of exclusion range (0-indexed, inclusive). `None` = last step | Note Step indices are 0-based. For a 3-step funnel `["A", "B", "C"]`, `from_step=0, to_step=2` covers the entire funnel. ## Holding Constant Hold properties constant across all funnel steps. Only users whose property value is the same at every step are counted as converting. ### String Shorthand Pass property names as strings: ``` # Only count conversions where platform is the same at every step result = ws.query_funnel( ["Signup", "Purchase"], holding_constant="platform", ) # Multiple properties result = ws.query_funnel( ["Signup", "Purchase"], holding_constant=["platform", "country"], ) ``` ### The `HoldingConstant` Class For user-profile properties, use `HoldingConstant` objects: ``` from mixpanel_headless import HoldingConstant result = ws.query_funnel( ["Signup", "Purchase"], holding_constant=[ HoldingConstant("platform"), # event property (default) HoldingConstant("plan_tier", resource_type="people"), # user-profile property ], ) ``` `HoldingConstant` fields: | Field | Type | Default | Description | | --------------- | ---------------------- | ---------- | ------------------------------------------------- | | `property` | `str` | (required) | Property name to hold constant | | `resource_type` | `"events" \| "people"` | `"events"` | Whether this is an event or user-profile property | Note Maximum 3 holding constant properties per query. ## Time Ranges ### Relative (Default) By default, `query_funnel()` returns the last 30 days. Customize with `last` (always in days) and `unit` (aggregation granularity): ``` # Last 7 days result = ws.query_funnel(["Signup", "Purchase"], last=7) # Last 84 days (~12 weeks), weekly granularity result = ws.query_funnel(["Signup", "Purchase"], last=84, unit="week") # Last 180 days (~6 months), monthly granularity result = ws.query_funnel(["Signup", "Purchase"], last=180, unit="month") ``` ### Absolute Specify explicit start and end dates: ``` # Q1 2025 result = ws.query_funnel( ["Signup", "Purchase"], from_date="2025-01-01", to_date="2025-03-31", ) ``` Dates must be in `YYYY-MM-DD` format. ## Display Modes The `mode` parameter controls result presentation: | Mode | Description | Use case | | ------------------- | -------------------------- | ------------------------------- | | `"steps"` (default) | Step-level conversion data | Standard funnel analysis | | `"trends"` | Conversion over time | Track funnel performance trends | | `"table"` | Tabular breakdown | Detailed segment comparison | ``` # Conversion trend over time result = ws.query_funnel( ["Signup", "Purchase"], mode="trends", last=90, unit="week", ) ``` ## Reentry Mode Control how users re-enter the funnel after conversion using the `reentry_mode` parameter: | Mode | Behavior | | -------------- | -------------------------------------------------------- | | `"default"` | Server default reentry behavior | | `"basic"` | Users can re-enter after their conversion window expires | | `"aggressive"` | Users can re-enter as soon as they convert | | `"optimized"` | Server-optimized reentry (best for most use cases) | ``` # Allow aggressive re-entry for repeat purchase funnels result = ws.query_funnel( ["Browse", "Add to Cart", "Purchase"], reentry_mode="aggressive", last=30, ) ``` Note Reentry mode only matters for funnels where the same user can convert multiple times. ## Period-over-Period Comparison Compare funnel conversion against a previous period using `TimeComparison`: ``` from mixpanel_headless import TimeComparison # Compare this week's funnel against last week result = ws.query_funnel( ["Signup", "Activate", "Purchase"], time_comparison=TimeComparison.relative("week"), last=7, ) ``` See [Insights > Period-over-Period Comparison](https://mixpanel.github.io/mixpanel-headless/guide/query/#period-over-period-comparison) for all `TimeComparison` factory methods. ## Data Group Scoping ``` # Scope funnel to a data group result = ws.query_funnel(["Signup", "Purchase"], data_group_id=42) ``` ## Working with Results ### `FunnelQueryResult` `query_funnel()` returns a `FunnelQueryResult` with: ``` result = ws.query_funnel(["Signup", "Add to Cart", "Purchase"]) # Overall conversion rate (first to last step) result.overall_conversion_rate # 0.12 (12%) # DataFrame (lazy, cached) result.df # step event count step_conv_ratio overall_conv_ratio avg_time avg_time_from_start # 1 1 Signup 1000 1.00 1.00 0.0 0.0 # 2 2 Add to Cart 450 0.45 0.45 1800.0 1800.0 # 3 3 Purchase 120 0.27 0.12 3600.0 5400.0 # Raw step data result.steps_data # list of dicts with step-level metrics result.series # raw API series data # Time range result.from_date # "2025-03-01" result.to_date # "2025-03-31" # Metadata result.computed_at # "2025-03-31T12:00:00.000000+00:00" result.meta # {"sampling_factor": 1.0, ...} # Generated bookmark params (for debugging or persistence) result.params # dict β€” the full bookmark JSON sent to API ``` ### DataFrame Structure The DataFrame has one row per funnel step: | Column | Description | | --------------------- | ------------------------------------------------------- | | `step` | Step number (1-indexed) | | `event` | Event name | | `count` | Number of users/events reaching this step | | `step_conv_ratio` | Conversion rate from previous step (1.0 for first step) | | `overall_conv_ratio` | Conversion rate from first step | | `avg_time` | Average time from previous step (seconds) | | `avg_time_from_start` | Average time from first step (seconds) | ### Persisting as a Saved Report The generated bookmark params can be saved as a Mixpanel report: ``` from mixpanel_headless import CreateBookmarkParams result = ws.query_funnel(["Signup", "Purchase"], conversion_window=7, last=90) ws.create_bookmark(CreateBookmarkParams( name="Signup β†’ Purchase Funnel (7d window)", bookmark_type="funnels", params=result.params, )) ``` ### Debugging Inspect `result.params` to see the exact bookmark JSON sent to the API: ``` import json result = ws.query_funnel(["Signup", "Purchase"]) print(json.dumps(result.params, indent=2)) ``` ## Validation `query_funnel()` validates all parameter combinations **before** making an API call and raises `BookmarkValidationError` with descriptive messages: | Rule | Error code | Error message | | ------------------------------------- | ----------------------------------------- | ----------------------------------------------------- | | Fewer than 2 steps | `F1_MIN_STEPS` | At least 2 steps are required | | More than 100 steps | `F1_MAX_STEPS` | Maximum 100 steps allowed | | Empty step event name | `F2_EMPTY_STEP_EVENT` | Step event name must be a non-empty string | | Control chars in step name | `F2_CONTROL_CHAR_STEP_EVENT` | Step event name contains control characters | | Non-positive conversion window | `F3_CONVERSION_WINDOW_POSITIVE` | conversion_window must be a positive integer | | Window exceeds max for unit | `F3_CONVERSION_WINDOW_MAX` | conversion_window exceeds maximum for unit | | Empty exclusion event | `F4_EMPTY_EXCLUSION_EVENT` | Exclusion event name must be non-empty | | Exclusion step order invalid | `F4_EXCLUSION_STEP_ORDER` | to_step must be > from_step | | Exclusion step out of bounds | `F4_EXCLUSION_STEP_BOUNDS` | Step index exceeds step count | | Invalid conversion window unit | `F7_INVALID_WINDOW_UNIT` | Must be one of: second, minute, ... | | Second unit requires window >= 2 | `F7_SECOND_MIN_WINDOW` | Must be at least 2 for seconds | | More than 3 holding constant | `F8_MAX_HOLDING_CONSTANT` | Maximum 3 holding_constant properties | | Session math without session unit | `F9_SESSION_MATH_REQUIRES_SESSION_WINDOW` | Requires conversion_window_unit='session' | | Property math missing math_property | `F10_MATH_MISSING_PROPERTY` | Property-aggregation math types require math_property | | Non-property math given math_property | `F11_MATH_REJECTS_PROPERTY` | Count/rate math types don't accept math_property | ## Complete Examples ### E-Commerce Funnel ``` import mixpanel_headless as mp from mixpanel_headless import FunnelStep, Filter, Exclusion, HoldingConstant ws = mp.Workspace() # Full purchase funnel with exclusions and filters result = ws.query_funnel( [ FunnelStep("View Product"), FunnelStep("Add to Cart"), FunnelStep( "Purchase", filters=[Filter.greater_than("amount", 0)], ), ], conversion_window=7, exclusions=[Exclusion("Remove from Cart", from_step=1, to_step=2)], holding_constant="platform", where=Filter.equals("country", "US"), group_by="platform", last=90, ) print(f"Overall conversion: {result.overall_conversion_rate:.1%}") print(result.df) ``` ### Onboarding Funnel ``` # Track user onboarding completion result = ws.query_funnel( [ "Create Account", "Verify Email", "Complete Profile", "First Action", ], conversion_window=3, conversion_window_unit="day", order="loose", math="conversion_rate_unique", last=30, unit="week", mode="trends", # track onboarding trends over time ) # Identify the biggest drop-off point for step_data in result.steps_data: print(f"{step_data['event']}: {step_data['step_conv_ratio']:.0%} step conversion") ``` ## Generating Params Without Querying Use `build_funnel_params()` to generate bookmark params without making an API call β€” useful for debugging, inspecting the generated JSON, or saving queries as reports: ``` # Same arguments as query_funnel(), returns dict instead of FunnelQueryResult params = ws.build_funnel_params( ["Signup", "Add to Cart", "Purchase"], conversion_window=7, exclusions=["Logout"], holding_constant="platform", last=90, ) import json print(json.dumps(params, indent=2)) # inspect the generated bookmark JSON # Save as a report directly from params from mixpanel_headless import CreateBookmarkParams ws.create_bookmark(CreateBookmarkParams( name="Purchase Funnel (7d)", bookmark_type="funnels", params=params, )) ``` ## Next Steps - [Insights Queries](https://mixpanel.github.io/mixpanel-headless/guide/query/index.md) β€” Typed analytics with DAU, formulas, filters, and breakdowns - [Retention Queries](https://mixpanel.github.io/mixpanel-headless/guide/query-retention/index.md) β€” Typed retention analysis with event pairs and custom buckets - [Flow Queries](https://mixpanel.github.io/mixpanel-headless/guide/query-flows/index.md) β€” Typed flow path analysis with steps, directions, and graph output - [Live Analytics β€” Funnels](https://mixpanel.github.io/mixpanel-headless/guide/live-analytics/#funnels) β€” Legacy funnel method (saved funnels by ID) - [API Reference β€” Workspace](https://mixpanel.github.io/mixpanel-headless/api/workspace/index.md) β€” Full method signatures - [API Reference β€” Types](https://mixpanel.github.io/mixpanel-headless/api/types/index.md) β€” FunnelStep, Exclusion, HoldingConstant, FunnelQueryResult details Copy markdown # Retention Queries Build typed retention analysis against Mixpanel's Insights engine β€” define born/return event pairs, retention periods, custom buckets, and segmentation inline without creating saved reports first. Recommended `Workspace.query_retention()` is the typed way to run retention analysis programmatically. It supports capabilities not available through the legacy `retention()` method, including per-event filters, custom retention buckets, alignment modes, display modes, and typed breakdowns. ## When to Use `query_retention()` `query_retention()` builds retention bookmark params and posts them to the Insights engine. The legacy `retention()` method queries the older Retention API endpoint. Use `query_retention()` when you need any of the capabilities in the right column: | Capability | Legacy `retention()` | `query_retention()` | | ------------------------ | --------------------------------------------- | ----------------------------------------- | | Basic cohort retention | `retention(born_event=..., return_event=...)` | `query_retention("Signup", "Login")` | | Per-event filters | Expression strings only | `RetentionEvent("Signup", filters=[...])` | | Custom retention buckets | Not available | `bucket_sizes=[1, 3, 7, 14, 30]` | | Alignment modes | Not available | `alignment="birth"` or `"interval_start"` | | Display modes | Not available | `mode="curve"`, `"trends"`, or `"table"` | | Typed filters | Expression strings | `where=Filter.equals("country", "US")` | | Property breakdowns | `on="country"` | `group_by="country"` | | Math types | Not available | `math="retention_rate"` or `"unique"` | | Save query as a report | N/A | `result.params` β†’ `create_bookmark()` | Use the legacy `retention()` when: - You need the older Query API response format β†’ `retention(born_event=..., return_event=...)` - You need `born_where` / `return_where` expression-string filters β†’ `retention(born_where='...')` ## Getting Started The simplest possible retention query β€” weekly retention over the last 30 days: ``` import mixpanel_headless as mp ws = mp.Workspace() result = ws.query_retention("Signup", "Login") print(result.average) # synthetic average across all cohorts print(result.df.head()) # cohort_date bucket count rate # 0 2025-01-01 0 1000 1.000000 # 1 2025-01-01 1 800 0.800000 # 2 2025-01-01 2 650 0.650000 ``` Add a time range and retention unit: ``` # Daily retention over the last 14 days result = ws.query_retention("Signup", "Login", retention_unit="day", last=14) # Monthly retention over the last 180 days result = ws.query_retention("Signup", "Login", retention_unit="month", last=180) # Specific date range result = ws.query_retention( "Signup", "Login", from_date="2025-01-01", to_date="2025-03-31", retention_unit="week", ) ``` ## Events ### Plain Strings The simplest way to define born and return events β€” pass event names as strings: ``` result = ws.query_retention("Signup", "Login") ``` The first argument is the **born event** (defines cohort membership) and the second is the **return event** (defines what counts as returning). ### The `RetentionEvent` Class For per-event configuration with filters, use `RetentionEvent` objects: ``` from mixpanel_headless import RetentionEvent, Filter result = ws.query_retention( RetentionEvent("Signup", filters=[Filter.equals("source", "organic")]), RetentionEvent("Login"), ) ``` `RetentionEvent` fields: | Field | Type | Default | Description | | -------------------- | ---------------------- | ---------- | -------------------------------------- | | `event` | `str` | (required) | Mixpanel event name | | `filters` | `list[Filter] \| None` | `None` | Per-event filter conditions | | `filters_combinator` | `"all" \| "any"` | `"all"` | How per-event filters combine (AND/OR) | Plain strings and `RetentionEvent` objects can be mixed freely: ``` result = ws.query_retention( "Signup", # plain string β€” no filters needed RetentionEvent("Purchase", filters=[Filter.greater_than("amount", 0)]), ) ``` ### Per-Event Filters Apply filters to individual events using `RetentionEvent.filters`. These restrict which events count for that specific role (born or return): ``` from mixpanel_headless import RetentionEvent, Filter result = ws.query_retention( RetentionEvent("Signup", filters=[Filter.equals("source", "organic")]), RetentionEvent("Purchase", filters=[ Filter.equals("country", "US"), Filter.greater_than("amount", 25), ]), ) ``` By default, multiple per-event filters combine with AND logic. Use `filters_combinator="any"` for OR logic: ``` result = ws.query_retention( "Signup", RetentionEvent( "Purchase", filters=[ Filter.equals("country", "US"), Filter.equals("country", "CA"), ], filters_combinator="any", # match US OR CA ), ) ``` See [Insights Queries β€” Filters](https://mixpanel.github.io/mixpanel-headless/guide/query/#filters) for the full list of `Filter` factory methods. ## Retention Unit Control the retention period granularity: | Unit | Description | | ------------------ | ------------------------- | | `"day"` | Daily retention buckets | | `"week"` (default) | Weekly retention buckets | | `"month"` | Monthly retention buckets | ``` # Daily retention result = ws.query_retention("Signup", "Login", retention_unit="day", last=14) # Weekly retention (default) result = ws.query_retention("Signup", "Login", retention_unit="week", last=90) # Monthly retention result = ws.query_retention("Signup", "Login", retention_unit="month", last=180) ``` ## Alignment The `alignment` parameter controls how retention periods are anchored: | Alignment | Behavior | | ------------------- | ------------------------------------------------------------------------ | | `"birth"` (default) | Each user's retention clock starts from their born event | | `"interval_start"` | Retention periods align to calendar boundaries (start of day/week/month) | ``` # Birth-aligned (default) β€” each user's clock starts individually result = ws.query_retention("Signup", "Login", alignment="birth") # Interval-aligned β€” retention periods snap to calendar boundaries result = ws.query_retention("Signup", "Login", alignment="interval_start") ``` ## Custom Buckets By default, retention uses uniform bucket sizes (bucket 0, 1, 2, ...). Use `bucket_sizes` for non-uniform retention periods: ``` # Custom day-based buckets: day 1, 3, 7, 14, 30 result = ws.query_retention( "Signup", "Login", retention_unit="day", bucket_sizes=[1, 3, 7, 14, 30], ) ``` Bucket sizes must be: - Positive integers - In strictly ascending order - Maximum 730 values ## Aggregation The `math` parameter controls what metric is computed: | Math type | What it measures | | ---------------------------- | ------------------------------------------------------------------- | | `"retention_rate"` (default) | Percentage of cohort retained per bucket (0.0–1.0) | | `"unique"` | Raw unique user count per bucket | | `"total"` | Total event count per retention bucket | | `"average"` | Average of a numeric property per bucket (requires `math_property`) | `RetentionMathType = Literal["retention_rate", "unique", "total", "average"]` ``` # Retention rate (default) result = ws.query_retention("Signup", "Login", math="retention_rate") # Raw unique user counts result = ws.query_retention("Signup", "Login", math="unique") # Total login events per retention bucket (not just unique users) result = ws.query_retention("Signup", "Login", math="total") # Average session duration per retention bucket result = ws.query_retention( "Signup", "Login", math="average", math_property="session_duration", ) ``` ## Filters ### Global Filters Apply filters across the entire query with `where=`: ``` from mixpanel_headless import Filter # Single filter result = ws.query_retention( "Signup", "Login", where=Filter.equals("country", "US"), ) # Multiple filters (AND logic) result = ws.query_retention( "Signup", "Login", where=[ Filter.equals("platform", "web"), Filter.is_true("is_premium"), ], ) ``` Global filters apply to the overall query. For event-specific filtering, use `RetentionEvent.filters` (see [Events β€” Per-Event Filters](#per-event-filters)). See [Insights Queries β€” Available Filter Methods](https://mixpanel.github.io/mixpanel-headless/guide/query/#available-filter-methods) for the complete filter reference. ### Cohort Filters Restrict retention analysis to users in a cohort: ``` from mixpanel_headless import Filter, CohortCriteria, CohortDefinition # Do power users retain better? result = ws.query_retention( "Signup", "Login", where=Filter.in_cohort(123, "Power Users"), retention_unit="week", last=90, ) # Inline cohort β€” define the segment on the fly organic = CohortDefinition( CohortCriteria.did_event("Signup", at_least=1, within_days=90, where=Filter.equals("source", "organic")) ) result = ws.query_retention( "Signup", "Purchase", where=Filter.in_cohort(organic, name="Organic Signups"), ) ``` See [Insights Queries β€” Cohort Filters](https://mixpanel.github.io/mixpanel-headless/guide/query/#cohort-filters) for the full cohort filter reference. ### Custom Property Filters Use saved or inline custom properties in retention filters: ``` from mixpanel_headless import Filter, CustomPropertyRef result = ws.query_retention( "Signup", "Login", where=Filter.greater_than(property=CustomPropertyRef(42), value=100), retention_unit="week", last=90, ) ``` Warning Custom property filters in retention `where=` may cause server errors due to a known Mixpanel API bug. Custom property breakdowns work reliably. See [Insights Queries β€” Custom Properties in Queries](https://mixpanel.github.io/mixpanel-headless/guide/query/#custom-properties-in-queries) for `InlineCustomProperty`, validation rules, and full options. ## Breakdowns Break down retention results by property values with `group_by`: ``` from mixpanel_headless import GroupBy # Simple string breakdown result = ws.query_retention("Signup", "Login", group_by="platform") # Multiple breakdowns result = ws.query_retention("Signup", "Login", group_by=["country", "platform"]) # Numeric bucketing result = ws.query_retention( "Signup", "Purchase", group_by=GroupBy("amount", property_type="number", bucket_size=50), ) ``` ### Cohort Breakdowns Segment retention by cohort membership β€” compare how a cohort retains vs. everyone else: ``` from mixpanel_headless import CohortBreakdown result = ws.query_retention( "Signup", "Login", group_by=CohortBreakdown(123, "Power Users"), retention_unit="week", last=90, ) ``` Note `query_retention()` does not support mixing `CohortBreakdown` with property `GroupBy` in the same `group_by` list. Use one or the other. See [Insights Queries β€” Cohort Breakdowns](https://mixpanel.github.io/mixpanel-headless/guide/query/#cohort-breakdowns) for inline definitions and options. ### Custom Property Breakdowns Break down retention results by a saved or inline custom property: ``` from mixpanel_headless import GroupBy, CustomPropertyRef result = ws.query_retention( "Signup", "Login", group_by=GroupBy(property=CustomPropertyRef(42), property_type="number"), retention_unit="week", last=90, ) ``` See [Insights Queries β€” Custom Properties in Queries](https://mixpanel.github.io/mixpanel-headless/guide/query/#custom-properties-in-queries) for `InlineCustomProperty` and full options. See [Insights Queries β€” Breakdowns](https://mixpanel.github.io/mixpanel-headless/guide/query/#breakdowns) for the full `GroupBy` reference. ## Time Ranges ### Relative (Default) By default, `query_retention()` returns the last 30 days. Customize with `last` (always in days) and `unit` (aggregation granularity): ``` # Last 7 days result = ws.query_retention("Signup", "Login", last=7) # Last 90 days, weekly granularity result = ws.query_retention("Signup", "Login", last=90, unit="week") # Last 180 days, monthly granularity result = ws.query_retention("Signup", "Login", last=180, unit="month") ``` ### Absolute Specify explicit start and end dates: ``` # Q1 2025 result = ws.query_retention( "Signup", "Login", from_date="2025-01-01", to_date="2025-03-31", ) ``` Dates must be in `YYYY-MM-DD` format. ## Display Modes The `mode` parameter controls result presentation: | Mode | Chart type | Use case | | ------------------- | --------------- | ------------------------------------- | | `"curve"` (default) | Retention curve | Standard retention analysis | | `"trends"` | Line chart | Track retention performance over time | | `"table"` | Table | Detailed cohort-level comparison | ``` # Retention curve (default) result = ws.query_retention("Signup", "Login", mode="curve") # Trends over time result = ws.query_retention( "Signup", "Login", mode="trends", last=90, unit="week", ) # Tabular format result = ws.query_retention("Signup", "Login", mode="table") ``` ## Unbounded Mode Control how users who perform the return event outside their retention bucket are counted using `unbounded_mode`: | Mode | Behavior | | ----------------------- | ------------------------------------------------------------------------ | | `"none"` | Standard counting β€” only count in the exact bucket (default) | | `"carry_back"` | If a user returns in bucket N, also count them in all prior buckets | | `"carry_forward"` | If a user returns in bucket N, also count them in all subsequent buckets | | `"consecutive_forward"` | Count forward from the last bucket where the user was active | ``` # Carry-forward: once a user returns, count them as retained in all later buckets result = ws.query_retention( "Signup", "Login", unbounded_mode="carry_forward", retention_unit="week", last=90, ) # Carry-back: if a user returns in W4, also count them in W1-W3 result = ws.query_retention( "Signup", "Login", unbounded_mode="carry_back", retention_unit="week", last=90, ) ``` Tip Unbounded modes are useful for products where engagement is irregular. `carry_forward` gives an optimistic view, `carry_back` fills gaps. ## Cumulative Retention Enable cumulative counting with `retention_cumulative=True`. In cumulative mode, each bucket shows the total number of users who returned at least once up to that bucket, rather than the count for that specific bucket. ``` # Cumulative: bucket N = users who returned at least once in buckets 0..N result = ws.query_retention( "Signup", "Login", retention_cumulative=True, retention_unit="week", last=90, ) ``` ## Period-over-Period Comparison Compare retention curves against a previous period using `TimeComparison`: ``` from mixpanel_headless import TimeComparison # Compare this month's retention against last month result = ws.query_retention( "Signup", "Login", time_comparison=TimeComparison.relative("month"), retention_unit="week", last=30, ) ``` See [Insights > Period-over-Period Comparison](https://mixpanel.github.io/mixpanel-headless/guide/query/#period-over-period-comparison) for all `TimeComparison` factory methods. ## Data Group Scoping ``` # Scope retention to a data group result = ws.query_retention("Signup", "Login", data_group_id=42) ``` ## Working with Results ### `RetentionQueryResult` `query_retention()` returns a `RetentionQueryResult` with: ``` result = ws.query_retention("Signup", "Login", retention_unit="week", last=90) # Cohort data β€” keyed by cohort date for date, data in result.cohorts.items(): print(f"{date}: {data['first']} users born") print(f" Retention: {data['rates']}") # [1.0, 0.8, 0.65, ...] # Synthetic average across all cohorts result.average # {"first": 500, "counts": [...], "rates": [...]} # DataFrame (lazy, cached) result.df # cohort_date bucket count rate # 0 2025-01-01 0 1000 1.000000 # 1 2025-01-01 1 800 0.800000 # 2 2025-01-01 2 650 0.650000 # 3 2025-01-08 0 950 1.000000 # 4 2025-01-08 1 760 0.800000 # Time range result.from_date # "2025-01-01" result.to_date # "2025-03-31" # Metadata result.computed_at # "2025-03-31T12:00:00.000000+00:00" result.meta # {"sampling_factor": 1.0, ...} # Generated bookmark params (for debugging or persistence) result.params # dict β€” the full bookmark JSON sent to API ``` ### DataFrame Structure The DataFrame has one row per (cohort_date, bucket) pair: | Column | Description | | ------------- | ---------------------------------------------------------------------- | | `cohort_date` | Date string identifying the cohort (users born on this date) | | `bucket` | Retention bucket index (0 = born period, 1 = first return period, ...) | | `count` | Number of users retained in this bucket | | `rate` | Retention rate for this bucket (count / cohort size, 0.0–1.0) | ### Cohort Data Structure Each entry in `result.cohorts` is a dict with: | Key | Type | Description | | -------- | ------------- | ------------------------------------------------------- | | `first` | `int` | Cohort size β€” users who did the born event on this date | | `counts` | `list[int]` | User counts retained per bucket | | `rates` | `list[float]` | Retention rates per bucket (0.0–1.0) | ### Persisting as a Saved Report The generated bookmark params can be saved as a Mixpanel report: ``` from mixpanel_headless import CreateBookmarkParams result = ws.query_retention("Signup", "Login", retention_unit="week", last=90) ws.create_bookmark(CreateBookmarkParams( name="Signup β†’ Login Retention (Weekly)", bookmark_type="retention", params=result.params, )) ``` ### Debugging Inspect `result.params` to see the exact bookmark JSON sent to the API: ``` import json result = ws.query_retention("Signup", "Login") print(json.dumps(result.params, indent=2)) ``` ## Validation `query_retention()` validates all parameter combinations **before** making an API call and raises `BookmarkValidationError` with descriptive messages: | Rule | Error code | Error message | | ----------------------------- | ------------------------------ | ------------------------------------------------------ | | Empty born event name | `R1_EMPTY_BORN_EVENT` | Born event name must be a non-empty string | | Control chars in born event | `R1_CONTROL_CHAR_BORN_EVENT` | Born event name contains control characters | | Empty return event name | `R2_EMPTY_RETURN_EVENT` | Return event name must be a non-empty string | | Control chars in return event | `R2_CONTROL_CHAR_RETURN_EVENT` | Return event name contains control characters | | Non-positive bucket sizes | `R5_BUCKET_SIZES_POSITIVE` | Each bucket size must be a positive integer | | Float bucket sizes | `R5_BUCKET_SIZES_INTEGER` | Bucket sizes must be integers, not floats | | Buckets not ascending | `R6_BUCKET_SIZES_ASCENDING` | Bucket sizes must be in strictly ascending order | | Invalid retention unit | `R7_INVALID_RETENTION_UNIT` | Must be one of: day, week, month | | Invalid alignment | `R8_INVALID_ALIGNMENT` | Must be one of: birth, interval_start | | Invalid math | `R9_INVALID_MATH` | Must be one of: retention_rate, unique, total, average | | Invalid mode | `R10_INVALID_MODE` | Must be one of: curve, trends, table | | Invalid unit | `R11_INVALID_UNIT` | Must be one of: day, week, month | Errors are collected β€” all validation issues are reported at once, not just the first: ``` from mixpanel_headless import BookmarkValidationError try: ws.query_retention("", "Login", bucket_sizes=[5, 3, 1]) except BookmarkValidationError as e: for error in e.errors: print(f"[{error.code}] {error.path}: {error.message}") # [R1_EMPTY_BORN_EVENT] born_event: Born event name must be a non-empty string # [R6_BUCKET_SIZES_ASCENDING] bucket_sizes: Bucket sizes must be in strictly ascending order ``` ## Complete Examples ### User Onboarding Retention ``` import mixpanel_headless as mp from mixpanel_headless import RetentionEvent, Filter ws = mp.Workspace() # Weekly retention: do new signups come back? result = ws.query_retention( RetentionEvent("Signup", filters=[Filter.equals("source", "organic")]), "Login", retention_unit="week", last=90, group_by="platform", ) # Inspect average retention curve avg = result.average print(f"Cohort size: {avg['first']}") for i, rate in enumerate(avg['rates']): print(f" Week {i}: {rate:.1%}") # Export to DataFrame for further analysis print(result.df) ``` ### Product Engagement ``` # Do users who complete onboarding keep making purchases? result = ws.query_retention( "Complete Onboarding", "Purchase", retention_unit="month", last=180, where=Filter.is_true("is_premium"), ) # Custom bucket sizes for key retention milestones milestone_retention = ws.query_retention( "Signup", "Login", retention_unit="day", bucket_sizes=[1, 3, 7, 14, 30, 60, 90], last=90, ) ``` ### Retention Trends ``` # Track how retention is changing over time result = ws.query_retention( "Signup", "Login", mode="trends", unit="week", retention_unit="week", last=180, ) print(result.df) ``` ## Generating Params Without Querying Use `build_retention_params()` to generate bookmark params without making an API call β€” useful for debugging, inspecting the generated JSON, or saving queries as reports: ``` # Same arguments as query_retention(), returns dict instead of RetentionQueryResult params = ws.build_retention_params( "Signup", "Login", retention_unit="week", bucket_sizes=[1, 3, 7, 14, 30], last=90, ) import json print(json.dumps(params, indent=2)) # inspect the generated bookmark JSON # Save as a report directly from params from mixpanel_headless import CreateBookmarkParams ws.create_bookmark(CreateBookmarkParams( name="Signup β†’ Login Retention (Custom Buckets)", bookmark_type="retention", params=params, )) ``` ## Next Steps - [Insights Queries](https://mixpanel.github.io/mixpanel-headless/guide/query/index.md) β€” Typed analytics with DAU, formulas, filters, and breakdowns - [Funnel Queries](https://mixpanel.github.io/mixpanel-headless/guide/query-funnels/index.md) β€” Typed funnel conversion analysis with steps, exclusions, and conversion windows - [Flow Queries](https://mixpanel.github.io/mixpanel-headless/guide/query-flows/index.md) β€” Typed flow path analysis with steps, directions, and graph output - [Live Analytics β€” Retention](https://mixpanel.github.io/mixpanel-headless/guide/live-analytics/#retention) β€” Legacy retention method - [API Reference β€” Workspace](https://mixpanel.github.io/mixpanel-headless/api/workspace/index.md) β€” Full method signatures - [API Reference β€” Types](https://mixpanel.github.io/mixpanel-headless/api/types/index.md) β€” RetentionEvent, RetentionQueryResult, RetentionAlignment, RetentionMode, RetentionMathType details Copy markdown # Flow Queries Build typed flow path analysis against Mixpanel's Insights engine β€” define anchor events, control forward/reverse step depth, apply per-step filters, and analyze user paths inline without creating saved reports first. Recommended `Workspace.query_flow()` is the typed way to run flow analysis programmatically. It supports capabilities not available through the legacy `query_saved_flows()` method, including per-step filters, direction controls, multiple visualization modes, NetworkX graph output, and typed result analysis. ## When to Use `query_flow()` `query_flow()` builds flow bookmark params and posts them to the Insights engine. The legacy `query_saved_flows()` method queries a pre-existing saved Flows report by bookmark ID. Use `query_flow()` when you need any of the capabilities in the right column: | Capability | Legacy `query_saved_flows()` | `query_flow()` | | ---------------------- | ------------------------------------- | ---------------------------------------------------------------- | | Basic flow analysis | `query_saved_flows(bookmark_id=123)` | `query_flow("Purchase")` | | Ad-hoc anchor events | Not available β€” requires saved report | `query_flow("Purchase")` or `query_flow(["Signup", "Purchase"])` | | Per-step filters | Not available | `FlowStep("Purchase", filters=[...])` | | Direction control | Not available | `forward=3, reverse=1` | | Per-step direction | Not available | `FlowStep("Purchase", forward=5)` | | Visualization modes | Not available | `mode="sankey"`, `"paths"`, or `"tree"` | | NetworkX graph | Not available | `result.graph` β€” full DiGraph | | Top transitions | Not available | `result.top_transitions(10)` | | Drop-off summary | Not available | `result.drop_off_summary()` | | Tree traversal | Not available | `result.trees` β€” recursive `FlowTreeNode` | | Save query as a report | N/A | `result.params` β†’ `create_bookmark()` | Use the legacy `query_saved_flows()` when: - You need to query an existing saved Flows report by bookmark ID β†’ `query_saved_flows(bookmark_id=123)` ## Getting Started The simplest possible flow query β€” what happens after a Purchase event, last 30 days: ``` import mixpanel_headless as mp ws = mp.Workspace() result = ws.query_flow("Purchase") print(result.nodes_df.head()) # step event type count anchor_type # 0 0 Purchase ANCHOR 5000 NORMAL # 1 1 View Receipt FORWARD 3200 NORMAL # 2 1 Add to Cart FORWARD 1800 NORMAL print(result.edges_df.head()) # source_step source_event target_step target_event count target_type # 0 0 Purchase 1 View Receipt 3200 FORWARD # 1 0 Purchase 1 Add to Cart 1800 FORWARD ``` Add direction controls and time range: ``` # 3 steps forward and 1 step back from Purchase result = ws.query_flow("Purchase", forward=3, reverse=1, last=90) # Top user paths print(result.top_transitions(5)) # [("Purchase", "View Receipt", 3200), ("View Receipt", "Checkout", 2100), ...] # Specific date range result = ws.query_flow( "Purchase", from_date="2025-01-01", to_date="2025-03-31", ) ``` ## Steps ### Plain Strings The simplest way to define anchor events β€” pass event names as strings: ``` # Single anchor event result = ws.query_flow("Purchase") # Multiple anchor events result = ws.query_flow(["Signup", "Purchase"]) ``` Each string becomes an anchor step in the flow β€” Mixpanel traces user paths forward and backward from these events. ### The `FlowStep` Class For per-step configuration with filters and direction overrides, use `FlowStep` objects: ``` from mixpanel_headless import FlowStep, Filter result = ws.query_flow( FlowStep( "Purchase", forward=5, reverse=2, filters=[Filter.greater_than("amount", 50)], ), ) ``` `FlowStep` fields: | Field | Type | Default | Description | | -------------------- | ---------------------- | ---------- | --------------------------------------------------- | | `event` | `str` | (required) | Mixpanel event name to anchor on | | `forward` | `int \| None` | `None` | Forward steps for this step (0-5, overrides global) | | `reverse` | `int \| None` | `None` | Reverse steps for this step (0-5, overrides global) | | `label` | `str \| None` | `None` | Display label (defaults to event name) | | `filters` | `list[Filter] \| None` | `None` | Per-step filter conditions | | `filters_combinator` | `"all" \| "any"` | `"all"` | How per-step filters combine (AND/OR) | Plain strings and `FlowStep` objects can be mixed freely: ``` result = ws.query_flow([ "Signup", # plain string β€” no overrides needed FlowStep("Purchase", filters=[Filter.equals("country", "US")]), ]) ``` ### Per-Step Filters Apply filters to individual steps using `FlowStep.filters`. These restrict which events count for that specific anchor: ``` from mixpanel_headless import FlowStep, Filter result = ws.query_flow( FlowStep( "Purchase", filters=[ Filter.equals("country", "US"), Filter.greater_than("amount", 25), ], ), ) ``` By default, multiple per-step filters combine with AND logic. Use `filters_combinator="any"` for OR logic: ``` result = ws.query_flow( FlowStep( "Purchase", filters=[ Filter.equals("country", "US"), Filter.equals("country", "CA"), ], filters_combinator="any", # match US OR CA ), ) ``` See [Insights Queries β€” Filters](https://mixpanel.github.io/mixpanel-headless/guide/query/#filters) for the full list of `Filter` factory methods. ### Filtering with `where=` Restrict flow analysis to a subset of users using the `where=` parameter. Flows support both cohort filters and property filters: ``` from mixpanel_headless import Filter, CohortCriteria, CohortDefinition # Cohort filter β€” what do power users do after purchasing? result = ws.query_flow( "Purchase", forward=3, where=Filter.in_cohort(123, "Power Users"), ) # Property filter β€” iOS users only result = ws.query_flow( "Purchase", where=Filter.equals("platform", "iOS"), last=30, ) # Inline cohort β€” what paths do frequent buyers take? frequent_buyers = CohortDefinition( CohortCriteria.did_event("Purchase", at_least=5, within_days=30) ) result = ws.query_flow( "Purchase", where=Filter.in_cohort(frequent_buyers, name="Frequent Buyers"), ) ``` Note Cohort breakdowns (`CohortBreakdown`) and custom properties (`CustomPropertyRef`, `InlineCustomProperty`) are not supported in flows. See [Insights Queries β€” Cohort Filters](https://mixpanel.github.io/mixpanel-headless/guide/query/#cohort-filters) for the full cohort filter reference and [Insights Queries β€” Filters](https://mixpanel.github.io/mixpanel-headless/guide/query/#filters) for all `Filter` factory methods. ## Direction Control how many steps forward and backward from the anchor Mixpanel traces: | Parameter | Range | Default | Description | | --------- | ----- | ------- | ------------------------------------ | | `forward` | 0–5 | 3 | Steps traced after the anchor event | | `reverse` | 0–5 | 0 | Steps traced before the anchor event | ``` # Forward-only (default) β€” what happens after Purchase? result = ws.query_flow("Purchase", forward=3) # Reverse-only β€” what led to Purchase? result = ws.query_flow("Purchase", forward=0, reverse=3) # Both directions β€” context around Purchase result = ws.query_flow("Purchase", forward=3, reverse=2) ``` At least one direction must be nonzero β€” a flow with `forward=0, reverse=0` raises a validation error. ### Per-Step Direction Overrides Each `FlowStep` can override the global direction settings: ``` from mixpanel_headless import FlowStep result = ws.query_flow( [ FlowStep("Signup", forward=5), # trace 5 steps forward from Signup FlowStep("Purchase", reverse=3), # trace 3 steps back from Purchase ], forward=2, # global default (used when step doesn't override) reverse=0, # global default ) ``` When a step provides `forward` or `reverse`, that value is used for that step. When `None`, the global value applies. ## Visualization Modes The `mode` parameter controls how flow data is structured and returned: | Mode | `flows_merge_type` | Use case | | -------------------- | ------------------ | --------------------------------------------------------- | | `"sankey"` (default) | `"graph"` | Aggregated node/edge graph β€” best for Sankey diagrams | | `"paths"` | `"list"` | Top user paths as ranked sequences | | `"tree"` | `"tree"` | Recursive tree from anchor β€” detailed node-level analysis | ``` # Sankey mode (default) β€” aggregated graph result = ws.query_flow("Purchase", mode="sankey") print(result.nodes_df) # node-level data print(result.edges_df) # edge-level transitions print(result.graph) # NetworkX DiGraph # Paths mode β€” top user paths result = ws.query_flow("Purchase", mode="paths") print(result.df) # ranked path sequences # Tree mode β€” recursive tree structure result = ws.query_flow("Purchase", mode="tree") for tree in result.trees: print(f"{tree.event}: {tree.total_count} users") for child in tree.children: print(f" β†’ {child.event}: {child.total_count}") ``` ## Conversion Window Control the maximum time between the first and last step in the flow: ``` # 7-day conversion window (default) result = ws.query_flow("Purchase", conversion_window=7) # 30-day window result = ws.query_flow("Purchase", conversion_window=30, conversion_window_unit="day") # Weekly window result = ws.query_flow("Purchase", conversion_window=2, conversion_window_unit="week") # Session-based window result = ws.query_flow("Purchase", conversion_window=1, conversion_window_unit="session") ``` | Unit | Description | | ----------------- | --------------------------- | | `"day"` (default) | Window measured in days | | `"week"` | Window measured in weeks | | `"month"` | Window measured in months | | `"session"` | Window measured in sessions | ## Count Type The `count_type` parameter controls how users are counted: | Count type | What it measures | | -------------------- | ----------------------------------------------------------- | | `"unique"` (default) | Unique users who traversed each path | | `"total"` | Total event occurrences (one user can count multiple times) | | `"session"` | Unique sessions containing the path | ``` # Unique users (default) result = ws.query_flow("Purchase", count_type="unique") # Total events result = ws.query_flow("Purchase", count_type="total") # Session-based result = ws.query_flow("Purchase", count_type="session") ``` ## Additional Options ### Data Group Scope flow analysis to a specific data group: ``` # Scope to a data group result = ws.query_flow("Purchase", data_group_id=42, last=30) ``` ### Cardinality Control how many unique next/previous events are shown per step. Higher values show more granular paths; lower values collapse rare paths: ``` # Show top 5 events per step (default is 3) result = ws.query_flow("Purchase", cardinality=5) # Maximum granularity result = ws.query_flow("Purchase", cardinality=50) ``` Range: 1–50. ### Collapse Repeated Events Merge consecutive occurrences of the same event into a single step: ``` # Collapse repeated events (e.g., multiple "Page View" in a row) result = ws.query_flow("Purchase", collapse_repeated=True) ``` ### Hidden Events Exclude specific events from the flow analysis: ``` # Hide noisy events from the flow result = ws.query_flow( "Purchase", hidden_events=["Session Start", "Page View", "Heartbeat"], ) ``` ## Time Ranges ### Relative (Default) By default, `query_flow()` returns the last 30 days. Customize with `last`: ``` # Last 7 days result = ws.query_flow("Purchase", last=7) # Last 90 days result = ws.query_flow("Purchase", last=90) ``` ### Absolute Specify explicit start and end dates: ``` # Q1 2025 result = ws.query_flow( "Purchase", from_date="2025-01-01", to_date="2025-03-31", ) ``` Dates must be in `YYYY-MM-DD` format. When `from_date` is provided without `to_date`, the end date defaults to today. ## Working with Results ### `FlowQueryResult` `query_flow()` returns a `FlowQueryResult` with mode-aware properties: ``` result = ws.query_flow("Purchase", forward=3, reverse=1, last=90) # Node data β€” one row per node in the flow result.nodes_df # step event type count anchor_type is_custom_event conversion_rate_change # 0 0 Purchase ANCHOR 5000 NORMAL False NaN # Edge data β€” transitions between nodes result.edges_df # source_step source_event target_step target_event count target_type # NetworkX graph β€” for programmatic path analysis g = result.graph print(f"Nodes: {g.number_of_nodes()}, Edges: {g.number_of_edges()}") # Top transitions by volume result.top_transitions(5) # [("Purchase", "View Receipt", 3200), ("View Receipt", "Checkout", 2100), ...] # Per-step drop-off summary result.drop_off_summary() # {"steps": [{"step": 0, "total": 5000, "dropoff": 0, "rate": 0.0}, ...]} # Overall conversion rate result.overall_conversion_rate # 0.42 # Visualization mode result.mode # "sankey" # API metadata result.computed_at # "2025-03-31T12:00:00.000000+00:00" result.meta # {"sampling_factor": 1.0, ...} # Generated bookmark params (for debugging or persistence) result.params # dict β€” the full bookmark JSON sent to API ``` ### Node DataFrame (`nodes_df`) One row per node in the flow graph: | Column | Description | | ------------------------ | --------------------------------------------------------------- | | `step` | Step index (0 = anchor, positive = forward, negative = reverse) | | `event` | Event name | | `type` | Node type: `ANCHOR`, `FORWARD`, `REVERSE`, `DROPOFF`, `PRUNED` | | `count` | Number of users at this node | | `anchor_type` | `NORMAL`, `RELATIVE_FORWARD`, or `RELATIVE_REVERSE` | | `is_custom_event` | Whether this is a computed/custom event | | `conversion_rate_change` | Change in conversion rate from previous step | ### Edge DataFrame (`edges_df`) One row per transition between nodes: | Column | Description | | -------------- | ---------------------------------------- | | `source_step` | Source node step index | | `source_event` | Source event name | | `target_step` | Target node step index | | `target_event` | Target event name | | `count` | Number of users who made this transition | | `target_type` | Target node type | ### NetworkX Graph (`graph`) A `networkx.DiGraph` for programmatic path analysis: ``` import networkx as nx g = result.graph # Shortest paths from anchor anchor_nodes = [n for n, d in g.nodes(data=True) if d.get("type") == "ANCHOR"] # All paths from anchor to a specific event for path in nx.all_simple_paths(g, source=anchor_nodes[0], target="Checkout@2"): print(" β†’ ".join(path)) # Highest-traffic edges for u, v, data in sorted(g.edges(data=True), key=lambda x: x[2]["count"], reverse=True)[:5]: print(f"{u} β†’ {v}: {data['count']} users") ``` Nodes are keyed as `"{event}@{step}"` with attributes `count`, `type`, and `step`. Edges have a `count` attribute. #### What the graph unlocks The DiGraph turns flow data into a structure that algorithms can reason about β€” answering questions no dashboard can: ``` import networkx as nx g = result.graph # "What's the shortest path from Signup to Purchase?" path = nx.shortest_path(g, "Signup@0", "Purchase@3") # β†’ ["Signup@0", "Browse@1", "Add to Cart@2", "Purchase@3"] # "Which event is the biggest bottleneck β€” the one most paths must pass through?" betweenness = nx.betweenness_centrality(g, weight="count") bottleneck = max(betweenness, key=betweenness.get) # "Are users looping back?" cycles = list(nx.simple_cycles(g)) # "What fraction of Add to Cart users actually Purchase?" cart_out = sum(d["count"] for _, _, d in g.out_edges("Add to Cart@2", data=True)) to_purchase = g.edges["Add to Cart@2", "Purchase@3"]["count"] micro_conversion = to_purchase / cart_out # "Which events are dead ends β€” reachable but leading nowhere?" dead_ends = [n for n in g.nodes() if g.out_degree(n) == 0 and g.in_degree(n) > 0] ``` Every graph theory algorithm in NetworkX β€” shortest paths, centrality, community detection, cycle detection, max-flow β€” works out of the box on flow data. This is particularly powerful for AI agents: they can programmatically explore path structure, identify optimization opportunities, and quantify the impact of removing or adding steps, without any visualization required. ### Tree Mode Results When `mode="tree"`, results include `FlowTreeNode` objects: ``` result = ws.query_flow("Purchase", mode="tree") for tree in result.trees: print(f"{tree.event}: {tree.total_count} users") print(f" Conversion: {tree.conversion_rate:.1%}") print(f" Drop-off: {tree.drop_off_count}") print(f" Depth: {tree.depth}") # Traverse children for child in tree.children: print(f" β†’ {child.event}: {child.total_count}") # All paths from root to leaves for path in tree.all_paths(): print(" β†’ ".join(node.event for node in path)) ``` `FlowTreeNode` fields: | Field | Type | Description | | ----------------- | -------------------------- | ------------------------------------------------------------- | | `event` | `str` | Event name | | `type` | `str` | `ANCHOR`, `NORMAL`, `DROPOFF`, `PRUNED`, `FORWARD`, `REVERSE` | | `step_number` | `int` | Zero-based step index | | `total_count` | `int` | Total users at this node | | `drop_off_count` | `int` | Users who dropped off | | `converted_count` | `int` | Users who continued | | `children` | `tuple[FlowTreeNode, ...]` | Child nodes | | `conversion_rate` | `float` | Property: `converted_count / total_count` | | `depth` | `int` | Property: maximum depth of subtree | | `node_count` | `int` | Property: total nodes in subtree | `FlowTreeNode` methods: | Method | Returns | Description | | -------------- | -------------------------- | ------------------------------------------------------------------------------------- | | `all_paths()` | `list[list[FlowTreeNode]]` | All root-to-leaf paths through the subtree | | `flatten()` | `list[FlowTreeNode]` | Preorder traversal of all nodes | | `find(event)` | `list[FlowTreeNode]` | All nodes matching an event name | | `render()` | `str` | Box-drawing ASCII visualization | | `to_dict()` | `dict` | JSON-serializable recursive dictionary | | `to_anytree()` | `AnyNode` | Convert to [`anytree`](https://anytree.readthedocs.io/) node for rendering and export | #### What the tree unlocks Tree mode gives each node its own `total_count`, `converted_count`, and `drop_off_count` β€” the full decision tree at every branching point. This lets you answer questions about *where exactly* users diverge: ``` result = ws.query_flow("Signup", mode="tree", forward=4) for tree in result.trees: # "At each step, what percentage of users take each branch?" for node in tree.flatten(): if node.children: print(f"\nAfter {node.event} ({node.total_count} users):") for child in sorted( node.children, key=lambda c: c.total_count, reverse=True ): pct = child.total_count / node.total_count * 100 print(f" β†’ {child.event}: {pct:.0f}% ({child.total_count})") # "What's the highest-converting complete path?" best_path = max(tree.all_paths(), key=lambda p: p[-1].converted_count) print(" β†’ ".join(f"{n.event}({n.conversion_rate:.0%})" for n in best_path)) # "Where is the single biggest drop-off?" worst = max(tree.flatten(), key=lambda n: n.drop_off_count) print(f"Biggest drop-off: {worst.event} β€” {worst.drop_off_count} users lost") # Render the full decision tree as ASCII print(tree.render()) # Purchase (5000) # β”œβ”€β”€ View Receipt (3200) # β”‚ β”œβ”€β”€ Rate App (1100) # β”‚ └── Browse More (2100) # └── Contact Support (800) # └── ⊘ Drop-off (800) ``` The tree is uniquely suited to *branching analysis* β€” understanding not just the top path, but what fraction of users chose each alternative at every fork. #### anytree Integration `FlowTreeNode` is frozen and children-only β€” you can traverse downward, but you can't ask a node "how did I get here?" Converting to [`anytree`](https://anytree.readthedocs.io/) adds **parent references**, **root-to-node paths**, **tree-wide search**, and **Graphviz export**, unlocking questions that require upward or lateral navigation. Use `to_anytree()` on any single `FlowTreeNode`, or `result.anytree` for the full list of converted roots: ``` from anytree import RenderTree, findall result = ws.query_flow("Purchase", mode="tree", forward=3) root = result.anytree[0] # converted AnyNode root # Render the full tree with counts for pre, _, node in RenderTree(root): print(f"{pre}{node.event} ({node.total_count})") # Purchase (5000) # β”œβ”€β”€ View Receipt (3200) # β”‚ β”œβ”€β”€ Rate App (1100) # β”‚ └── Browse More (2100) # └── Contact Support (800) ``` ##### Tracing backward from any node The killer feature: given a node deep in the tree, trace the exact path that led there β€” something `FlowTreeNode` alone can't do. ``` from anytree import findall # Find all drop-off points and trace how users got there dropoffs = findall(root, filter_=lambda n: n.type == "DROPOFF") for node in dropoffs: # .path gives the full root-to-node chain journey = " β†’ ".join(n.event for n in node.path) print(f"{journey} ({node.drop_off_count} users lost)") # Parent references β€” "what came immediately before this event?" support = findall(root, filter_=lambda n: n.event == "Contact Support")[0] print(f"{support.event} ← {support.parent.event}") # Contact Support ← Purchase ``` ##### Node introspection Every converted node exposes properties that make tree analysis concise: ``` from anytree import findall checkout = findall(root, filter_=lambda n: n.event == "Checkout")[0] checkout.depth # 2 β€” distance from anchor checkout.ancestors # (Purchase, View Receipt) β€” full chain above checkout.siblings # other events at the same branching point checkout.is_leaf # True if no further steps follow root.leaves # all terminal nodes β€” drop-offs and end-of-funnel root.height # deepest path length in the tree ``` ##### Graphviz export Export the flow tree as a visual diagram β€” especially useful for sharing with stakeholders or embedding in reports: ``` from anytree.exporter import UniqueDotExporter # Basic export β€” renders to PNG via Graphviz UniqueDotExporter(root).to_picture("flow.png") # Custom formatting β€” size nodes by user count, highlight drop-offs UniqueDotExporter( root, nodenamefunc=lambda n: f"{n.event}\n({n.total_count:,})", nodeattrfunc=lambda n: ( 'shape=box, style=filled, fillcolor="#ff6b6b"' if n.type == "DROPOFF" else 'shape=box, style=filled, fillcolor="#4ecdc4"' ), ).to_picture("flow_colored.png") ``` Note Graphviz must be installed on your system (`brew install graphviz` / `apt install graphviz`) for `to_picture()` to work. Alternatively, use `to_dotfile("flow.dot")` to generate the DOT source for rendering elsewhere. ### Persisting as a Saved Report The generated bookmark params can be saved as a Mixpanel report: ``` from mixpanel_headless import CreateBookmarkParams result = ws.query_flow("Purchase", forward=3, reverse=1) ws.create_bookmark(CreateBookmarkParams( name="Purchase Flow Analysis", bookmark_type="flows", params=result.params, )) ``` ### Debugging Inspect `result.params` to see the exact bookmark JSON sent to the API: ``` import json result = ws.query_flow("Purchase") print(json.dumps(result.params, indent=2)) ``` ## Validation `query_flow()` validates all parameter combinations **before** making an API call and raises `BookmarkValidationError` with descriptive messages: | Rule | Error code | Error message | | --------------------------- | ----------------------------- | -------------------------------------------------- | | No steps provided | `FL1_EMPTY_STEPS` | At least one step is required | | Empty step event name | `FL2_EMPTY_STEP_EVENT` | Step event name must be a non-empty string | | Control chars in event name | `FL2_CONTROL_CHAR_STEP_EVENT` | Step event name contains control characters | | Forward out of range | `FL3_FORWARD_RANGE` | forward must be between 0 and 5 | | Reverse out of range | `FL4_REVERSE_RANGE` | reverse must be between 0 and 5 | | Both directions zero | `FL5_NO_DIRECTION` | At least one of forward or reverse must be nonzero | | Cardinality out of range | `FL6_CARDINALITY_RANGE` | cardinality must be between 1 and 50 | | Invalid count type | `FL7_INVALID_COUNT_TYPE` | Must be one of: unique, total, session | | Invalid window unit | `FL8_INVALID_WINDOW_UNIT` | Must be one of: day, week, month, session | | Invalid date range | `FL9_INVALID_DATE_RANGE` | Invalid date range configuration | | Invalid mode | `FL10_INVALID_MODE` | Must be one of: sankey, paths, tree | Errors are collected β€” all validation issues are reported at once, not just the first: ``` from mixpanel_headless import BookmarkValidationError try: ws.query_flow("", forward=10, reverse=-1) except BookmarkValidationError as e: for error in e.errors: print(f"[{error.code}] {error.path}: {error.message}") # [FL2_EMPTY_STEP_EVENT] steps[0].event: Step event name must be a non-empty string # [FL3_FORWARD_RANGE] forward: forward must be between 0 and 5 # [FL4_REVERSE_RANGE] reverse: reverse must be between 0 and 5 ``` ## Complete Examples ### E-Commerce Checkout Flow ``` import mixpanel_headless as mp from mixpanel_headless import FlowStep, Filter ws = mp.Workspace() # What happens after users add items to cart? result = ws.query_flow( FlowStep( "Add to Cart", forward=4, filters=[Filter.greater_than("item_price", 10)], ), conversion_window=7, last=90, hidden_events=["Session Start", "Page View"], ) # Top conversion paths for src, tgt, count in result.top_transitions(10): print(f" {src} β†’ {tgt}: {count:,} users") # Drop-off analysis summary = result.drop_off_summary() for step in summary["steps"]: print(f" Step {step['step']}: {step['dropoff']:,} dropped ({step['rate']:.1%})") ``` ### User Onboarding Paths ``` # What do new users do after signing up? result = ws.query_flow( "Signup", forward=5, cardinality=10, last=30, collapse_repeated=True, ) # Visualize as NetworkX graph g = result.graph print(f"Discovered {g.number_of_nodes()} unique steps") print(f"Discovered {g.number_of_edges()} unique transitions") # Find most common path from signup import networkx as nx for node in g.successors("Signup@0"): weight = g.edges["Signup@0", node]["count"] print(f" Signup β†’ {node}: {weight:,} users") ``` ### Reverse Flow Analysis ``` # What led users to churn? result = ws.query_flow( "Cancel Subscription", forward=0, reverse=5, count_type="unique", last=90, ) # Analyze the reverse path print(result.nodes_df[result.nodes_df["type"] == "REVERSE"]) ``` ### Tree Mode Exploration ``` # Detailed tree analysis of purchase paths result = ws.query_flow("Purchase", mode="tree", forward=3) for tree in result.trees: print(f"\nAnchor: {tree.event} ({tree.total_count:,} users)") print(f" Tree depth: {tree.depth}") print(f" Total nodes: {tree.node_count}") # Print all complete paths for path in tree.all_paths(): path_str = " β†’ ".join(f"{n.event}({n.total_count})" for n in path) print(f" {path_str}") ``` ## Generating Params Without Querying Use `build_flow_params()` to generate bookmark params without making an API call β€” useful for debugging, inspecting the generated JSON, or saving queries as reports: ``` # Same arguments as query_flow(), returns dict instead of FlowQueryResult params = ws.build_flow_params( "Purchase", forward=3, reverse=1, conversion_window=7, last=90, ) import json print(json.dumps(params, indent=2)) # inspect the generated bookmark JSON # Save as a report directly from params from mixpanel_headless import CreateBookmarkParams ws.create_bookmark(CreateBookmarkParams( name="Purchase Flow (3 forward, 1 reverse)", bookmark_type="flows", params=params, )) ``` ## Flow Segments Break flow results down by a property, cohort, or frequency using the `segments` parameter: ``` # Break down paths by platform result = ws.query_flow("Purchase", segments="platform", last=30) # Segment by a GroupBy with bucketing from mixpanel_headless import GroupBy result = ws.query_flow( "Purchase", segments=GroupBy("revenue", property_type="number", bucket_size=50), last=30, ) # Segment by cohort membership from mixpanel_headless import CohortBreakdown result = ws.query_flow( "Purchase", segments=CohortBreakdown(cohort_id=123, name="Power Users"), last=30, ) ``` `segments` accepts a string (property name), `GroupBy`, `CohortBreakdown`, `FrequencyBreakdown`, or a list of these. ## Flow Exclusions Hide specific events from flow paths using the `exclusions` parameter: ``` # Exclude noisy events from the flow result = ws.query_flow( "Purchase", exclusions=["Page View", "Session Start"], forward=3, last=30, ) ``` Excluded events are removed from the flow graph β€” they won't appear as nodes or edges, making it easier to see meaningful user paths. ## Session Events Anchor a flow step to a session boundary using `FlowStep.session_event`: ``` from mixpanel_headless import FlowStep # What happens after a session starts? result = ws.query_flow( FlowStep(event="Session Start", session_event="start"), forward=5, last=30, ) # What leads up to a session ending? result = ws.query_flow( FlowStep(event="Session End", session_event="end"), reverse=3, last=30, ) ``` Values: `"start"` (session start anchor) or `"end"` (session end anchor). ## Next Steps - [Insights Queries](https://mixpanel.github.io/mixpanel-headless/guide/query/index.md) β€” Typed analytics with DAU, formulas, filters, and breakdowns - [Funnel Queries](https://mixpanel.github.io/mixpanel-headless/guide/query-funnels/index.md) β€” Typed funnel conversion analysis with steps, exclusions, and conversion windows - [Retention Queries](https://mixpanel.github.io/mixpanel-headless/guide/query-retention/index.md) β€” Typed retention analysis with event pairs and custom buckets - [Live Analytics β€” Flows](https://mixpanel.github.io/mixpanel-headless/guide/live-analytics/#flows) β€” Legacy saved Flows report method - [API Reference β€” Workspace](https://mixpanel.github.io/mixpanel-headless/api/workspace/index.md) β€” Full method signatures - [API Reference β€” Types](https://mixpanel.github.io/mixpanel-headless/api/types/index.md) β€” FlowStep, FlowTreeNode, FlowQueryResult details Copy markdown # User Profile Queries Query user profiles from Mixpanel's Engage API β€” filter by properties, sort, select fields, count matching profiles, and fetch large result sets with parallel pagination. Uses the same `Filter` vocabulary as all other query engines. Recommended `Workspace.query_user()` is the 5th engine in the unified query system. It answers **identity** questions ("who are these users?") that complement the behavioral questions answered by insights, funnels, retention, and flows. ## When to Use `query_user()` Use `query_user()` when you need to work with **user profiles** rather than events: | Use Case | Example | | --------------------------- | ------------------------------------------------------------------------------- | | Filter profiles by property | `query_user(mode="profiles", where=Filter.equals("plan", "premium"))` | | Count matching profiles | `query_user(where=Filter.is_set("$email"))` | | Get top users by a metric | `query_user(mode="profiles", sort_by="ltv", sort_order="descending", limit=50)` | | Look up specific users | `query_user(mode="profiles", distinct_id="user_abc123")` | | Profile a behavioral cohort | `query_user(mode="profiles", cohort=CohortDefinition.all_of(...))` | | Export profiles at scale | `query_user(mode="profiles", properties=[...], limit=5000, parallel=True)` | | Cross-engine profiling | Insights identifies a segment, `query_user()` profiles those users | Use `stream_profiles()` when you need to iterate over raw profile dicts without structured filtering or DataFrame output. ## Getting Started ``` import mixpanel_headless as mp from mixpanel_headless import Filter ws = mp.Workspace() # Quick count β€” how many profiles exist? (default: mode="aggregate") result = ws.query_user() print(f"Total profiles: {result.value}") # Quick peek β€” one sample profile result = ws.query_user(mode="profiles") print(result.df) # Filter and select properties result = ws.query_user( mode="profiles", where=Filter.equals("plan", "premium"), properties=["$email", "$name", "ltv"], sort_by="ltv", sort_order="descending", limit=50, ) print(result.df) # distinct_id | last_seen | email | name | ltv ``` ## Aggregate Mode Aggregate mode is the default (`mode="aggregate"`). Compute statistics across matching profiles without fetching individual records: ``` # Count users with email (aggregate is the default mode) count = ws.query_user(where=Filter.is_set("$email")) print(f"Users with email: {count.value}") # Total users (all) total = ws.query_user() print(f"Total profiles: {total.value}") # Extremes (min/max) of a numeric property result = ws.query_user( aggregate="extremes", aggregate_property="ltv", ) print(result.aggregate_data) # {"max": 9500, "min": 0, ...} # Percentile result = ws.query_user( aggregate="percentile", aggregate_property="ltv", percentile=90, ) print(result.aggregate_data) # {"percentile": 90, "result": 4500} # Numeric summary (count, mean, variance, sum_of_squares) result = ws.query_user( aggregate="numeric_summary", aggregate_property="ltv", ) print(result.aggregate_data) # {"count": 1532, "mean": 245.6, ...} # Segmented count by cohort IDs result = ws.query_user( segment_by=[12345, 67890], ) print(result.df) # columns: segment, value ``` ## Behavioral Filtering Filter by behavioral criteria using the same `CohortDefinition` builders available across all engines: ``` from mixpanel_headless import CohortDefinition, CohortCriteria # Users who purchased 3+ times in 30 days result = ws.query_user( mode="profiles", cohort=CohortDefinition.all_of( CohortCriteria.did_event("Purchase", at_least=3, within_days=30), ), properties=["$email", "plan", "ltv"], limit=200, ) print(f"Power buyers: {len(result.profiles)}") # Filter by saved cohort ID result = ws.query_user(mode="profiles", cohort=12345, limit=100) ``` ## Parallel Fetching For large result sets, enable concurrent page retrieval: ``` result = ws.query_user( mode="profiles", where=Filter.is_set("$email"), properties=["$email", "plan", "ltv"], limit=5000, parallel=True, workers=5, ) print(f"Fetched {len(result.profiles)} profiles") print(f"Pages: {result.meta['pages_fetched']}, Workers: {result.meta['workers']}") ``` ## Cross-Engine Composition The real power of `query_user()` is combining it with behavioral engines. Identify interesting behavior with event engines, then profile those users: ``` # Step 1: Which plan drives the most DAU? dau = ws.query("Login", math="dau", group_by="plan", last=30) top_plan = dau.df.sort_values("count", ascending=False).iloc[0]["event"] # Step 2: Profile users from that plan users = ws.query_user( mode="profiles", where=Filter.equals("plan", top_plan), properties=["$email", "company", "ltv"], sort_by="ltv", sort_order="descending", limit=100, ) print(f"Plan '{top_plan}' has {len(users.profiles)} top users") print(users.df.describe()) ``` ## UserQueryResult All results are returned as `UserQueryResult`, a frozen dataclass with: | Property | Type | Description | | --------------- | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `.df` | `pd.DataFrame` | Lazy cached DataFrame. Profiles mode: `distinct_id`, `last_seen`, then alphabetical properties (`$` prefix stripped). Aggregate mode: `metric`/`value` columns. | | `.total` | `int` | Number of profiles returned (`len(profiles)`). Use `mode='aggregate', aggregate='count'` for full population count. | | `.profiles` | `list[dict]` | Normalized profile dicts | | `.distinct_ids` | `list[str]` | List of distinct IDs from profiles | | `.value` | `int \| float \| None` | Scalar aggregate result (aggregate mode only) | | `.params` | `dict` | Engage API params used (for debugging) | | `.meta` | `dict` | Execution metadata (session_id, pages_fetched, parallel, workers) | | `.to_dict()` | `dict` | JSON-serializable output | ## Previewing Parameters Inspect the generated Engage API params without executing: ``` params = ws.build_user_params( where=Filter.equals("plan", "premium"), properties=["$email", "ltv"], sort_by="ltv", ) import json print(json.dumps(params, indent=2)) ``` ## What's Next - [Unified Query System](https://mixpanel.github.io/mixpanel-headless/guide/unified-query-system/index.md) β€” how all five engines work together - [Insights Queries](https://mixpanel.github.io/mixpanel-headless/guide/query/index.md) β€” event-level analytics - [Funnel Queries](https://mixpanel.github.io/mixpanel-headless/guide/query-funnels/index.md) β€” conversion analysis - [Retention Queries](https://mixpanel.github.io/mixpanel-headless/guide/query-retention/index.md) β€” cohort retention - [Flow Queries](https://mixpanel.github.io/mixpanel-headless/guide/query-flows/index.md) β€” path analysis - [API Reference](https://mixpanel.github.io/mixpanel-headless/api/workspace/index.md) β€” full method signatures Copy markdown # Live Analytics Query Mixpanel's analytics APIs directly for real-time data. Looking for Insights Queries? For DAU/WAU/MAU, multi-metric comparison, formulas, per-user aggregation, rolling windows, percentiles, typed filters, or numeric breakdowns, see **[Insights Queries](https://mixpanel.github.io/mixpanel-headless/guide/query/index.md)** β€” the recommended way to run analytics queries programmatically. Explore on DeepWiki πŸ€– **[Querying Data Guide β†’](https://deepwiki.com/mixpanel/mixpanel-headless/3.2.4-querying-data)** Ask questions about segmentation, funnels, retention, JQL, or other live query methods. Looking for entity management? To create, update, or delete dashboards, reports, or cohorts, see the [Entity Management guide](https://mixpanel.github.io/mixpanel-headless/guide/entity-management/index.md). ## When to Use Live Queries Use live queries when: - You need the most current data - You're running one-off analysis - The query is already optimized by Mixpanel (segmentation, funnels, retention) - You want to leverage Mixpanel's pre-computed aggregations Use local queries when: - You need to run many queries over the same data - You need custom SQL logic - You want to minimize API calls - Context window preservation matters (for AI agents) ## Segmentation Time-series event counts with optional property segmentation: ``` import mixpanel_headless as mp ws = mp.Workspace() # Simple count over time result = ws.segmentation( event="Purchase", from_date="2025-01-01", to_date="2025-01-31" ) # Segment by property result = ws.segmentation( event="Purchase", from_date="2025-01-01", to_date="2025-01-31", on="country" ) # With filtering result = ws.segmentation( event="Purchase", from_date="2025-01-01", to_date="2025-01-31", on="country", where='properties["plan"] == "premium"', unit="week" # day, week, month ) # Access as DataFrame print(result.df) ``` ``` # Simple segmentation mp query segmentation --event Purchase --from 2025-01-01 --to 2025-01-31 # With property breakdown mp query segmentation --event Purchase --from 2025-01-01 --to 2025-01-31 \ --on country --format table # Filter with jq to get just the total mp query segmentation --event Purchase --from 2025-01-01 --to 2025-01-31 \ --format json --jq '.total' # Get top 3 days by volume mp query segmentation --event Purchase --from 2025-01-01 --to 2025-01-31 \ --format json --jq '.series | to_entries | sort_by(.value) | reverse | .[:3]' ``` ### SegmentationResult ``` result.event # "Purchase" result.dates # ["2025-01-01", "2025-01-02", ...] result.values # {"$overall": [100, 150, ...]} result.segments # ["US", "UK", "DE", ...] result.df # pandas DataFrame result.to_dict() # JSON-serializable dict ``` ## Funnels Looking for Ad-Hoc Funnel Queries? For typed funnel definitions with per-step filters, exclusions, holding constant properties, and conversion window control β€” without creating a saved funnel first β€” see **[Funnel Queries](https://mixpanel.github.io/mixpanel-headless/guide/query-funnels/index.md)**. Analyze conversion through a sequence of saved steps: ``` # First, find your funnel ID funnels = ws.funnels() for f in funnels: print(f"{f.funnel_id}: {f.name}") # Query the funnel result = ws.funnel( funnel_id=12345, from_date="2025-01-01", to_date="2025-01-31" ) # With segmentation result = ws.funnel( funnel_id=12345, from_date="2025-01-01", to_date="2025-01-31", on="country" ) # Access results for step in result.steps: print(f"{step.event}: {step.count} ({step.conversion_rate:.1%})") ``` ``` # List available funnels mp inspect funnels # Query a funnel mp query funnel --funnel-id 12345 --from 2025-01-01 --to 2025-01-31 --format table ``` ### FunnelResult ``` result.funnel_id # 12345 result.steps # [FunnelStep, ...] result.overall_rate # 0.15 (15% overall conversion) result.df # DataFrame with step metrics # Each step step.event # "Checkout Started" step.count # 5000 step.conversion_rate # 0.85 step.avg_time # timedelta or None ``` ## Retention Looking for Typed Retention Queries? For per-event filters, custom retention buckets, alignment modes, display modes, typed breakdowns, and persistable results β€” without expression-string filters β€” see **[Retention Queries](https://mixpanel.github.io/mixpanel-headless/guide/query-retention/index.md)**. Cohort-based retention analysis: ``` result = ws.retention( born_event="Signup", return_event="Login", from_date="2025-01-01", to_date="2025-01-31", born_where='properties["source"] == "organic"', unit="week" ) # Access cohorts for cohort in result.cohorts: print(f"{cohort.date}: {cohort.size} users") print(f" Retention: {cohort.retention_rates}") ``` ``` mp query retention \ --born-event Signup \ --return-event Login \ --from 2025-01-01 \ --to 2025-01-31 \ --unit week \ --format table ``` ### RetentionResult ``` result.born_event # "Signup" result.return_event # "Login" result.cohorts # [CohortInfo, ...] result.df # DataFrame with retention matrix # Each cohort cohort.date # "2025-01-01" cohort.size # 1000 cohort.retention_rates # [1.0, 0.45, 0.32, 0.28, ...] ``` ## JQL (JavaScript Query Language) Run custom JQL scripts for advanced analysis: ``` script = """ function main() { return Events({ from_date: params.from_date, to_date: params.to_date, event_selectors: [{event: "Purchase"}] }) .groupBy(["properties.country"], mixpanel.reducer.count()) .sortDesc("value") .take(10); } """ result = ws.jql( script=script, params={"from_date": "2025-01-01", "to_date": "2025-01-31"} ) print(result.data) # Raw JQL result print(result.df) # As DataFrame ``` ``` # From file mp query jql --script ./query.js --param from_date=2025-01-01 --param to_date=2025-01-31 # Inline mp query jql --script 'function main() { return Events({...}).count(); }' ``` ## Event Counts Multi-event time series comparison: ``` result = ws.event_counts( events=["Signup", "Purchase", "Churn"], from_date="2025-01-01", to_date="2025-01-31", unit="day" ) # DataFrame with columns: date, Signup, Purchase, Churn print(result.df) ``` ``` mp query event-counts \ --event Signup --event Purchase --event Churn \ --from 2025-01-01 --to 2025-01-31 \ --format table ``` ## Property Counts Break down an event by property values: ``` result = ws.property_counts( event="Purchase", property_name="country", from_date="2025-01-01", to_date="2025-01-31", limit=10 ) print(result.df) # Columns: date, US, UK, DE, ... ``` ``` mp query property-counts \ --event Purchase \ --property country \ --from 2025-01-01 --to 2025-01-31 \ --limit 10 \ --format table ``` ## Activity Feed Get a user's event history: ``` result = ws.activity_feed( distinct_ids=["user_123", "user_456"], from_date="2025-01-01", to_date="2025-01-31" ) for event in result.events: print(f"{event.time}: {event.event}") print(f" Properties: {event.properties}") ``` ``` mp query activity-feed \ --distinct-id user_123 \ --from 2025-01-01 --to 2025-01-31 \ --format json ``` ## Saved Reports Query saved reports from Mixpanel (Insights, Retention, Funnels, and Flows). ### Listing Bookmarks First, find available saved reports: ``` # List all saved reports bookmarks = ws.list_bookmarks() for b in bookmarks: print(f"{b.id}: {b.name} ({b.type})") # Filter by type insights = ws.list_bookmarks(bookmark_type="insights") funnels = ws.list_bookmarks(bookmark_type="funnels") ``` ``` mp inspect bookmarks mp inspect bookmarks --type insights mp inspect bookmarks --type funnels --format table ``` ### Querying Saved Reports Query Insights, Retention, or Funnel reports by bookmark ID: Get Bookmark IDs First Run `list_bookmarks()` or `mp inspect bookmarks` to find the numeric ID of the report you want to query. ``` # Get the bookmark ID from list_bookmarks() first bookmarks = ws.list_bookmarks(bookmark_type="insights") bookmark_id = bookmarks[0].id # e.g., 98765 result = ws.query_saved_report(bookmark_id=bookmark_id) print(f"Report type: {result.report_type}") print(result.df) ``` ``` # First find your bookmark ID mp inspect bookmarks --type insights --format table # Then query it mp query saved-report --bookmark-id 98765 --format table ``` ## Flows Query saved Flows reports: Prefer `query_flow()` for New Code The typed [`query_flow()`](https://mixpanel.github.io/mixpanel-headless/guide/query-flows/index.md) method lets you define flow analysis inline with per-step filters, direction controls, visualization modes, and NetworkX graph output β€” no saved report required. Use `query_saved_flows()` only when querying an existing saved Flows report. Flows Use Different IDs Flows reports have their own bookmark IDs. Filter with `--type flows` when listing. ``` # Get Flows bookmark ID flows = ws.list_bookmarks(bookmark_type="flows") bookmark_id = flows[0].id # e.g., 54321 result = ws.query_saved_flows(bookmark_id=bookmark_id) print(f"Conversion rate: {result.overall_conversion_rate:.1%}") for step in result.steps: print(f" {step}") ``` ``` # First find Flows bookmark IDs mp inspect bookmarks --type flows --format table # Then query it mp query flows --bookmark-id 54321 --format table ``` ## Frequency Analysis Analyze how often users perform an event: ``` result = ws.frequency( event="Login", from_date="2025-01-01", to_date="2025-01-31", unit="month", addiction_unit="day" ) # Distribution of logins per day print(result.buckets) # {"0": 1000, "1": 500, "2-3": 300, ...} ``` ``` mp query frequency \ --event Login \ --from 2025-01-01 --to 2025-01-31 \ --format table ``` ## Numeric Aggregations Aggregate numeric properties: ### Bucketing ``` result = ws.segmentation_numeric( event="Purchase", from_date="2025-01-01", to_date="2025-01-31", on="amount", type="general" # or "linear", "logarithmic" ) ``` ### Sum ``` result = ws.segmentation_sum( event="Purchase", from_date="2025-01-01", to_date="2025-01-31", on="amount" ) # Total revenue per time period ``` ### Average ``` result = ws.segmentation_average( event="Purchase", from_date="2025-01-01", to_date="2025-01-31", on="amount" ) # Average purchase amount per time period ``` ## API Escape Hatch For Mixpanel APIs not covered by the Workspace class, use the `api` property to make authenticated requests directly: ``` import mixpanel_headless as mp ws = mp.Workspace() client = ws.api # Example: List annotations from the Annotations API # Many Mixpanel APIs require the project ID in the URL path base_url = "https://mixpanel.com/api/app" # Use eu.mixpanel.com for EU url = f"{base_url}/projects/{client.project_id}/annotations" response = client.request("GET", url) annotations = response["results"] for ann in annotations: print(f"{ann['id']}: {ann['date']} - {ann['description']}") # Get a specific annotation by ID if annotations: annotation_id = annotations[0]["id"] detail_url = f"{base_url}/projects/{client.project_id}/annotations/{annotation_id}" annotation = client.request("GET", detail_url) print(annotation) ``` ### Request Parameters ``` client.request( "POST", "https://mixpanel.com/api/some/endpoint", params={"key": "value"}, # Query parameters json_body={"data": "payload"}, # JSON request body headers={"X-Custom": "header"}, # Additional headers timeout=60.0 # Request timeout in seconds ) ``` Authentication is handled automatically β€” the client adds the proper `Authorization` header to all requests. The client also exposes `project_id` and `region` properties, which are useful when constructing URLs for APIs that require these values in the path. ## Next Steps - [Data Discovery](https://mixpanel.github.io/mixpanel-headless/guide/discovery/index.md) β€” Explore your event schema - [API Reference](https://mixpanel.github.io/mixpanel-headless/api/workspace/index.md) β€” Complete API documentation Copy markdown # Streaming Data Stream events and user profiles directly from Mixpanel. Ideal for ETL pipelines, data processing, exports, and Unix-style piping. Explore on DeepWiki πŸ€– **[Data Flow Patterns β†’](https://deepwiki.com/mixpanel/mixpanel-headless/4.1-data-flow-patterns)** Ask questions about streaming, memory-efficient processing, or ETL pipeline patterns. ## Streaming Events ### Basic Usage Stream all events for a date range: ``` import mixpanel_headless as mp ws = mp.Workspace() for event in ws.stream_events( from_date="2025-01-01", to_date="2025-01-31" ): print(f"{event['event_name']}: {event['distinct_id']}") # event_time is a datetime object # properties contains remaining fields ws.close() ``` ### Filtering Events Filter by event name or expression: ``` # Filter by event names for event in ws.stream_events( from_date="2025-01-01", to_date="2025-01-31", events=["Purchase", "Signup"] ): process(event) # Filter with WHERE clause for event in ws.stream_events( from_date="2025-01-01", to_date="2025-01-31", where='properties["country"]=="US"' ): process(event) ``` ### Raw API Format By default, streaming returns normalized data with `event_time` as a datetime. Use `raw=True` to get the exact Mixpanel API format: ``` for event in ws.stream_events( from_date="2025-01-01", to_date="2025-01-31", raw=True ): # event has {"event": "...", "properties": {...}} structure # properties["time"] is Unix timestamp legacy_system.ingest(event) ``` ## Streaming Profiles ### Basic Usage Stream all user profiles: ``` for profile in ws.stream_profiles(): sync_to_crm(profile) ``` ### Filtering Profiles ``` for profile in ws.stream_profiles( where='properties["plan"]=="premium"' ): send_survey(profile) ``` ### Streaming Specific Users Stream a single user by their distinct ID: ``` for profile in ws.stream_profiles(distinct_id="user_123"): process(profile) ``` Stream multiple specific users: ``` user_ids = ["user_123", "user_456", "user_789"] for profile in ws.stream_profiles(distinct_ids=user_ids): sync_to_external_system(profile) ``` Mutually Exclusive `distinct_id` and `distinct_ids` cannot be used together. Use `distinct_id` for a single user, `distinct_ids` for multiple users. ### Streaming Group Profiles Stream group profiles (e.g., companies, accounts) instead of user profiles: ``` # Stream all company profiles for company in ws.stream_profiles(group_id="companies"): sync_company(company) # Filter group profiles for account in ws.stream_profiles( group_id="accounts", where='properties["plan"]=="enterprise"' ): process_enterprise_account(account) ``` ### Behavioral Filtering Stream users based on actions they've performed. Behaviors use a named pattern that you reference in a `where` clause: ``` # Users who completed a purchase in last 30 days behaviors = [{ "window": "30d", "name": "made_purchase", "event_selectors": [{"event": "Purchase"}] }] for profile in ws.stream_profiles( behaviors=behaviors, where='(behaviors["made_purchase"] > 0)' ): send_thank_you(profile) # Users who signed up but didn't purchase behaviors = [ {"window": "30d", "name": "signed_up", "event_selectors": [{"event": "Signup"}]}, {"window": "30d", "name": "purchased", "event_selectors": [{"event": "Purchase"}]} ] for profile in ws.stream_profiles( behaviors=behaviors, where='(behaviors["signed_up"] > 0) and (behaviors["purchased"] == 0)' ): send_conversion_reminder(profile) ``` Behavior Format Each behavior requires: `window` (time window like "30d"), `name` (identifier for `where` clause), and `event_selectors` (array with `{"event": "Name"}`). Mutually Exclusive `behaviors` cannot be used with `cohort_id`. Use one or the other for filtering. ### Historical Profile State Query profile state at a specific point in time: ``` import time # Profile state from 7 days ago seven_days_ago = int(time.time()) - (7 * 24 * 60 * 60) for profile in ws.stream_profiles(as_of_timestamp=seven_days_ago): compare_historical_state(profile) ``` ### Cohort Membership Analysis Get all users with cohort membership marked: ``` # Stream all users, marking which are in the cohort for profile in ws.stream_profiles( cohort_id="12345", include_all_users=True ): if profile.get("in_cohort"): tag_as_cohort_member(profile) else: tag_as_non_member(profile) ``` Requires cohort_id `include_all_users` only works when `cohort_id` is specified. ## Processing Patterns Use Python to filter, count, and export streamed data: ``` import json import mixpanel_headless as mp ws = mp.Workspace() # Filter to specific events purchases = [ e for e in ws.stream_events(from_date="2025-01-01", to_date="2025-01-31") if e["event_name"] == "Purchase" ] # Count events count = sum(1 for _ in ws.stream_events(from_date="2025-01-01", to_date="2025-01-31")) # Save to JSONL file with open("events.jsonl", "w") as f: for event in ws.stream_events(from_date="2025-01-01", to_date="2025-01-31"): f.write(json.dumps(event) + "\n") # Extract specific fields distinct_ids = [ p["distinct_id"] for p in ws.stream_profiles() ] ws.close() ``` ## Output Formats ### Normalized Format (Default) Events: ``` { "event_name": "Purchase", "distinct_id": "user_123", "event_time": "2025-01-15T10:30:00+00:00", "insert_id": "abc123", "properties": { "amount": 99.99, "currency": "USD" } } ``` Profiles: ``` { "distinct_id": "user_123", "last_seen": "2025-01-15T14:30:00", "properties": { "name": "Alice", "plan": "premium" } } ``` ### Raw Format (`raw=True`) Events: ``` { "event": "Purchase", "properties": { "distinct_id": "user_123", "time": 1705319400, "$insert_id": "abc123", "amount": 99.99, "currency": "USD" } } ``` Profiles: ``` { "$distinct_id": "user_123", "$properties": { "$last_seen": "2025-01-15T14:30:00", "name": "Alice", "plan": "premium" } } ``` ## Common Patterns ### ETL Pipeline Batch events and send to external system: ``` import mixpanel_headless as mp from your_warehouse import send_batch ws = mp.Workspace() batch = [] for event in ws.stream_events(from_date="2025-01-01", to_date="2025-01-31"): batch.append(event) if len(batch) >= 1000: send_batch(batch) batch = [] # Send remaining if batch: send_batch(batch) ws.close() ``` ### Aggregation Without Storage Compute statistics without creating a local table: ``` from collections import Counter import mixpanel_headless as mp ws = mp.Workspace() event_counts = Counter() for event in ws.stream_events(from_date="2025-01-01", to_date="2025-01-31"): event_counts[event["event_name"]] += 1 print(event_counts.most_common(10)) ws.close() ``` ### Context Manager Use `with` for automatic cleanup: ``` import mixpanel_headless as mp with mp.Workspace() as ws: for event in ws.stream_events(from_date="2025-01-01", to_date="2025-01-31"): process(event) # No need to call ws.close() ``` ## Method Signatures ### stream_events() ``` def stream_events( *, from_date: str, to_date: str, events: list[str] | None = None, where: str | None = None, raw: bool = False, ) -> Iterator[dict[str, Any]] ``` | Parameter | Type | Description | | ----------- | ------------------- | -------------------------- | | `from_date` | `str` | Start date (YYYY-MM-DD) | | `to_date` | `str` | End date (YYYY-MM-DD) | | `events` | `list[str] \| None` | Event names to include | | `where` | `str \| None` | Mixpanel expression filter | | `raw` | `bool` | Return raw API format | ### stream_profiles() ``` def stream_profiles( *, where: str | None = None, cohort_id: str | None = None, output_properties: list[str] | None = None, raw: bool = False, distinct_id: str | None = None, distinct_ids: list[str] | None = None, group_id: str | None = None, behaviors: list[dict[str, Any]] | None = None, as_of_timestamp: int | None = None, include_all_users: bool = False, ) -> Iterator[dict[str, Any]] ``` | Parameter | Type | Description | | ------------------- | -------------------- | ------------------------------------- | | `where` | `str \| None` | Mixpanel expression filter | | `cohort_id` | `str \| None` | Filter by cohort membership | | `output_properties` | `list[str] \| None` | Limit returned properties | | `raw` | `bool` | Return raw API format | | `distinct_id` | `str \| None` | Single user ID to fetch | | `distinct_ids` | `list[str] \| None` | Multiple user IDs to fetch | | `group_id` | `str \| None` | Group type for group profiles | | `behaviors` | `list[dict] \| None` | Behavioral filters | | `as_of_timestamp` | `int \| None` | Historical state Unix timestamp | | `include_all_users` | `bool` | Include all users with cohort marking | **Parameter Constraints:** - `distinct_id` and `distinct_ids` are mutually exclusive - `behaviors` and `cohort_id` are mutually exclusive - `include_all_users` requires `cohort_id` to be set ## Next Steps - [Live Analytics](https://mixpanel.github.io/mixpanel-headless/guide/live-analytics/index.md) β€” Real-time Mixpanel reports Copy markdown # Entity Management Manage Mixpanel dashboards, reports (bookmarks), cohorts, feature flags, experiments, alerts, annotations, and webhooks programmatically. Full CRUD operations with bulk support. For data governance operations (Lexicon definitions, drop filters, custom properties, custom events, and lookup tables), see the [Data Governance guide](https://mixpanel.github.io/mixpanel-headless/guide/data-governance/index.md). Prerequisites Entity management requires **authentication** β€” service account or OAuth credentials. **Scoping differs by entity type:** - **Dashboards, reports, cohorts, alerts, annotations, webhooks** require a **workspace ID** β€” set via `MP_WORKSPACE_ID` env var, `--workspace` / `-w` CLI flag, `Workspace(workspace=N)`, or `ws.use(workspace=N)`. List available workspaces with `mp workspace list` or `ws.workspaces()`. - **Feature flags and experiments** are **project-scoped** and do NOT require a workspace ID. ## Dashboards ### List Dashboards ``` import mixpanel_headless as mp ws = mp.Workspace() # List all dashboards dashboards = ws.list_dashboards() for d in dashboards: print(f"{d.id}: {d.title}") # Filter by specific IDs dashboards = ws.list_dashboards(ids=[123, 456]) ``` ``` # List all dashboards mp dashboards list # Filter by IDs mp dashboards list --ids 123,456 # Table format for quick scanning mp dashboards list --format table ``` ### Create a Dashboard ``` new_dash = ws.create_dashboard(mp.CreateDashboardParams( title="Q1 Metrics", description="Quarterly performance overview", )) print(f"Created dashboard {new_dash.id}: {new_dash.title}") ``` ``` mp dashboards create --title "Q1 Metrics" --description "Quarterly performance overview" ``` ### Get, Update, Delete ``` # Get a dashboard by ID dash = ws.get_dashboard(123) # Update updated = ws.update_dashboard(123, mp.UpdateDashboardParams( title="Q1 Metrics (Updated)", description="Revised quarterly overview", )) # Delete ws.delete_dashboard(123) # Bulk delete ws.bulk_delete_dashboards([123, 456, 789]) ``` ``` # Get details mp dashboards get 123 # Update mp dashboards update 123 --title "Q1 Metrics (Updated)" # Delete mp dashboards delete 123 # Bulk delete mp dashboards bulk-delete --ids 123,456,789 ``` ### Favorites and Pins ``` ws.favorite_dashboard(123) ws.unfavorite_dashboard(123) ws.pin_dashboard(123) ws.unpin_dashboard(123) ``` ``` mp dashboards favorite 123 mp dashboards unfavorite 123 mp dashboards pin 123 mp dashboards unpin 123 ``` ### Add a Report to a Dashboard ``` updated_dash = ws.add_report_to_dashboard( dashboard_id=123, bookmark_id=456 ) ``` ``` mp dashboards add-report 123 456 ``` ### Remove a Report from a Dashboard ``` updated_dash = ws.remove_report_from_dashboard( dashboard_id=123, bookmark_id=456 ) ``` ``` mp dashboards remove-report 123 456 ``` ### Blueprint Dashboards Create dashboards from pre-built templates: ``` # List available templates templates = ws.list_blueprint_templates() for t in templates: print(f"{t.title_key}: {t.number_of_reports} reports") # Create from template dash = ws.create_blueprint(template_type="product_analytics") # Configure and finalize config = ws.get_blueprint_config(dash.id) ws.finalize_blueprint(mp.BlueprintFinishParams( dashboard_id=dash.id, cards=config.cards, )) ``` ``` # List templates mp dashboards blueprints # Create from template mp dashboards blueprint-create --template-type product_analytics ``` ### Advanced Dashboard Operations ``` # Create an RCA (Root Cause Analysis) dashboard rca_dash = ws.create_rca_dashboard(mp.CreateRcaDashboardParams( rca_source_id=123, rca_source_data=mp.RcaSourceData(source_type="metric"), )) # Get ERF metrics erf = ws.get_dashboard_erf(dashboard_id=123) # Update dashboard components ws.update_report_link( dashboard_id=123, report_link_id=456, params=mp.UpdateReportLinkParams(link_type="embedded"), ) ws.update_text_card( dashboard_id=123, text_card_id=789, params=mp.UpdateTextCardParams(markdown="## Updated Header"), ) ``` ``` # Get ERF metrics mp dashboards erf 123 # Update text card mp dashboards update-text-card 123 --text-card-id 789 --markdown "## Updated" ``` ______________________________________________________________________ ## Reports (Bookmarks) Reports in Mixpanel are stored as "bookmarks". Each bookmark has a type (insights, funnels, flows, retention, etc.) and a params JSON object defining the query. ### List Reports ``` # List all reports reports = ws.list_bookmarks_v2() # Filter by type insights = ws.list_bookmarks_v2(bookmark_type="insights") funnels = ws.list_bookmarks_v2(bookmark_type="funnels") # Filter by IDs specific = ws.list_bookmarks_v2(ids=[123, 456]) for r in reports: print(f"{r.id}: {r.name} ({r.bookmark_type})") ``` ``` # List all reports mp reports list # Filter by type mp reports list --type insights mp reports list --type funnels # Filter by IDs mp reports list --ids 123,456 ``` ### Create a Report ``` report = ws.create_bookmark(mp.CreateBookmarkParams( name="Daily Signups", bookmark_type="insights", description="Track daily signup volume", params={"event": "Signup"}, )) print(f"Created report {report.id}: {report.name}") ``` ``` mp reports create \ --name "Daily Signups" \ --type insights \ --description "Track daily signup volume" \ --params '{"event": "Signup"}' ``` ### Get, Update, Delete ``` # Get report = ws.get_bookmark(123) # Update updated = ws.update_bookmark(123, mp.UpdateBookmarkParams( name="Daily Signups v2", description="Updated tracking", )) # Delete ws.delete_bookmark(123) # Bulk operations ws.bulk_delete_bookmarks([123, 456]) ws.bulk_update_bookmarks([ mp.BulkUpdateBookmarkEntry(id=123, name="Renamed Report"), mp.BulkUpdateBookmarkEntry(id=456, description="Updated desc"), ]) ``` ``` # Get mp reports get 123 # Update mp reports update 123 --name "Daily Signups v2" # Delete mp reports delete 123 # Bulk delete mp reports bulk-delete --ids 123,456 ``` ### Report History and Dashboard Links ``` # View change history history = ws.get_bookmark_history(bookmark_id=123) for entry in history.data: print(entry) # Paginate through history history = ws.get_bookmark_history(bookmark_id=123, page_size=10) if history.pagination.has_more: next_page = ws.get_bookmark_history( bookmark_id=123, cursor=history.pagination.cursor, ) # Find which dashboards contain this report dashboard_ids = ws.bookmark_linked_dashboard_ids(bookmark_id=123) ``` ``` # View history mp reports history 123 # Get linked dashboards mp reports linked-dashboards 123 ``` ______________________________________________________________________ ## Cohorts ### List Cohorts ``` # List all cohorts cohorts = ws.list_cohorts_full() # Filter by data group cohorts = ws.list_cohorts_full(data_group_id="default") # Filter by IDs cohorts = ws.list_cohorts_full(ids=[123, 456]) for c in cohorts: print(f"{c.id}: {c.name} ({c.count} users)") ``` ``` # List all cohorts mp cohorts list # Filter by data group mp cohorts list --data-group-id default # Filter by IDs mp cohorts list --ids 123,456 ``` ### Create a Cohort ``` cohort = ws.create_cohort(mp.CreateCohortParams( name="Power Users", description="Users with 10+ sessions in last 30 days", )) print(f"Created cohort {cohort.id}: {cohort.name}") ``` ``` mp cohorts create --name "Power Users" --description "Users with 10+ sessions" ``` ### Get, Update, Delete ``` # Get cohort = ws.get_cohort(123) # Update updated = ws.update_cohort(123, mp.UpdateCohortParams( name="Super Users", description="Updated criteria", )) # Delete ws.delete_cohort(123) # Bulk operations ws.bulk_delete_cohorts([123, 456]) ws.bulk_update_cohorts([ mp.BulkUpdateCohortEntry(id=123, name="Renamed Cohort"), mp.BulkUpdateCohortEntry(id=456, description="Updated"), ]) ``` ``` # Get mp cohorts get 123 # Update mp cohorts update 123 --name "Super Users" # Delete mp cohorts delete 123 # Bulk delete mp cohorts bulk-delete --ids 123,456 ``` ______________________________________________________________________ ## Feature Flags Feature flags are **project-scoped** β€” no workspace ID required. They use **UUID string IDs** (not integer IDs like dashboards/reports/cohorts). PUT Semantics Feature flag `update` uses **full replacement** (PUT semantics). All required fields (`name`, `key`, `status`, `ruleset`) must be provided on every update β€” even if you're only changing one field. ### List Feature Flags ``` import mixpanel_headless as mp ws = mp.Workspace() # List all flags (excludes archived by default) flags = ws.list_feature_flags() for f in flags: print(f"{f.name} ({f.key}): {f.status.value}") # Include archived flags flags = ws.list_feature_flags(include_archived=True) ``` ``` # List all flags mp flags list # Include archived mp flags list --include-archived # Table format mp flags list --format table ``` ### Create a Feature Flag ``` flag = ws.create_feature_flag(mp.CreateFeatureFlagParams( name="Dark Mode", key="dark_mode", description="Enable dark mode UI", tags=["ui", "frontend"], )) print(f"Created flag {flag.id}: {flag.key}") ``` ``` mp flags create --name "Dark Mode" --key dark_mode \ --description "Enable dark mode UI" --tags "ui,frontend" ``` ### Get, Update, Delete ``` # Get a flag by UUID flag = ws.get_feature_flag("abc-123-uuid") # Update (PUT β€” all required fields must be provided) updated = ws.update_feature_flag("abc-123-uuid", mp.UpdateFeatureFlagParams( name="Dark Mode", key="dark_mode", status=mp.FeatureFlagStatus.ENABLED, ruleset=flag.ruleset, # Must provide complete ruleset )) # Delete ws.delete_feature_flag("abc-123-uuid") ``` ``` # Get details mp flags get abc-123-uuid # Update (all required fields) mp flags update abc-123-uuid \ --name "Dark Mode" --key dark_mode \ --status enabled --ruleset '{"variants": [], "rollout": []}' # Delete mp flags delete abc-123-uuid ``` ### Archive, Restore, Duplicate ``` # Archive (soft-delete) ws.archive_feature_flag("abc-123-uuid") # Restore an archived flag restored = ws.restore_feature_flag("abc-123-uuid") # Duplicate copy = ws.duplicate_feature_flag("abc-123-uuid") ``` ``` mp flags archive abc-123-uuid mp flags restore abc-123-uuid mp flags duplicate abc-123-uuid ``` ### Test Users and History ``` # Assign test users to specific variants ws.set_flag_test_users("abc-123-uuid", mp.SetTestUsersParams( users={"On": "user-1", "Off": "user-2"} )) # View change history history = ws.get_flag_history("abc-123-uuid") print(f"{history.count} changes") # Check account limits limits = ws.get_flag_limits() print(f"Using {limits.current_usage}/{limits.limit} flags") ``` ``` # Set test users mp flags set-test-users abc-123-uuid \ --users '{"On": "user-1", "Off": "user-2"}' # View history mp flags history abc-123-uuid # Check limits mp flags limits ``` ______________________________________________________________________ ## Experiments Experiments are **project-scoped** β€” no workspace ID required. They have a distinct lifecycle with managed state transitions: ``` Draft β†’ Active (launch) β†’ Concluded (conclude) β†’ Success/Fail (decide) ``` PATCH Semantics Experiment `update` uses **partial update** (PATCH semantics). Only provide the fields you want to change. ### List Experiments ``` import mixpanel_headless as mp ws = mp.Workspace() experiments = ws.list_experiments() for e in experiments: print(f"{e.name}: {e.status.value if e.status else 'unknown'}") # Include archived experiments = ws.list_experiments(include_archived=True) ``` ``` mp experiments list mp experiments list --include-archived ``` ### Create an Experiment ``` exp = ws.create_experiment(mp.CreateExperimentParams( name="Checkout Flow Test", description="Test simplified checkout", hypothesis="Simpler checkout increases conversions by 10%", )) print(f"Created experiment {exp.id}: {exp.name}") ``` ``` mp experiments create --name "Checkout Flow Test" \ --description "Test simplified checkout" \ --hypothesis "Simpler checkout increases conversions by 10%" ``` ### Get, Update, Delete ``` # Get exp = ws.get_experiment("xyz-456-uuid") # Update (PATCH β€” only changed fields) updated = ws.update_experiment("xyz-456-uuid", mp.UpdateExperimentParams( description="Updated hypothesis and metrics", )) # Delete ws.delete_experiment("xyz-456-uuid") ``` ``` mp experiments get xyz-456-uuid mp experiments update xyz-456-uuid --description "Updated" mp experiments delete xyz-456-uuid ``` ### Experiment Lifecycle The key differentiator of experiments: a managed lifecycle with state transitions. ``` # 1. Create (starts in Draft) exp = ws.create_experiment(mp.CreateExperimentParams( name="Pricing Page Test" )) # 2. Launch (Draft β†’ Active) launched = ws.launch_experiment(exp.id) # 3. Conclude (Active β†’ Concluded) concluded = ws.conclude_experiment(exp.id) # Or with an explicit end date: concluded = ws.conclude_experiment( exp.id, params=mp.ExperimentConcludeParams(end_date="2026-04-01"), ) # 4. Decide (Concluded β†’ Success or Fail) decided = ws.decide_experiment(exp.id, mp.ExperimentDecideParams( success=True, variant="simplified", message="15% conversion lift confirmed", )) ``` ``` # Launch mp experiments launch xyz-456-uuid # Conclude mp experiments conclude xyz-456-uuid mp experiments conclude xyz-456-uuid --end-date 2026-04-01 # Decide as success mp experiments decide xyz-456-uuid --success \ --variant simplified --message "15% conversion lift confirmed" # Decide as failure mp experiments decide xyz-456-uuid --no-success \ --message "No significant difference" ``` ### Archive, Restore, Duplicate ``` ws.archive_experiment("xyz-456-uuid") restored = ws.restore_experiment("xyz-456-uuid") dup = ws.duplicate_experiment( "xyz-456-uuid", mp.DuplicateExperimentParams(name="Pricing Page Test v2"), ) ``` ``` mp experiments archive xyz-456-uuid mp experiments restore xyz-456-uuid mp experiments duplicate xyz-456-uuid --name "Pricing Page Test v2" ``` ### ERF Experiments List experiments in ERF (Experiment Results Framework) format: ``` erf = ws.list_erf_experiments() ``` ``` mp experiments erf ``` ______________________________________________________________________ ## Alerts Custom alerts monitor saved reports and notify when conditions are met. Alerts are **workspace-scoped** and linked to bookmarks (saved reports). ### List Alerts ``` import mixpanel_headless as mp ws = mp.Workspace() # List all alerts alerts = ws.list_alerts() for a in alerts: print(f"{a.id}: {a.name} (paused={a.paused})") # Filter by linked bookmark alerts = ws.list_alerts(bookmark_id=12345) ``` ``` # List all alerts mp alerts list # Filter by bookmark mp alerts list --bookmark-id 12345 # Table format mp alerts list --format table ``` ### Create an Alert ``` alert = ws.create_alert(mp.CreateAlertParams( bookmark_id=12345, name="Daily signups drop", condition={ "keys": [{"header": "Signup", "value": "Signup"}], "type": "absolute", "op": "<", "value": 100, }, frequency=mp.AlertFrequencyPreset.DAILY, paused=False, subscriptions=[{"type": "email", "value": "team@example.com"}], )) print(f"Created alert {alert.id}: {alert.name}") ``` ``` mp alerts create \ --bookmark-id 12345 \ --name "Daily signups drop" \ --condition '{"keys": [{"header": "Signup", "value": "Signup"}], "type": "absolute", "op": "<", "value": 100}' \ --frequency 86400 \ --subscriptions '[{"type": "email", "value": "team@example.com"}]' ``` ### Get, Update, Delete ``` # Get alert = ws.get_alert(42) # Update (PATCH semantics) updated = ws.update_alert(42, mp.UpdateAlertParams( name="Updated alert name", paused=True, )) # Delete ws.delete_alert(42) # Bulk delete ws.bulk_delete_alerts([42, 43, 44]) ``` ``` # Get mp alerts get 42 # Update mp alerts update 42 --name "Updated alert name" --paused # Delete mp alerts delete 42 # Bulk delete mp alerts bulk-delete --ids 42,43,44 ``` ### Monitoring ``` # Check alert count and limits count = ws.get_alert_count() print(f"{count.anomaly_alerts_count}/{count.alert_limit} alerts") # View trigger history (paginated) history = ws.get_alert_history(42, page_size=10) for entry in history.results: print(entry) # Send a test notification result = ws.test_alert(mp.CreateAlertParams( bookmark_id=12345, name="Test", condition={"type": "absolute", "op": "<", "value": 100}, frequency=86400, paused=False, subscriptions=[{"type": "email", "value": "me@example.com"}], )) # Get screenshot URL screenshot = ws.get_alert_screenshot_url("gcs-key-here") print(screenshot.signed_url) ``` ``` # Alert count and limits mp alerts count # Trigger history mp alerts history 42 --page-size 10 # Test notification mp alerts test \ --bookmark-id 12345 \ --name "Test" \ --condition '{"type": "absolute", "op": "<", "value": 100}' \ --frequency 86400 \ --subscriptions '[{"type": "email", "value": "me@example.com"}]' # Screenshot URL mp alerts screenshot --gcs-key "gcs-key-here" ``` ### Validate Alerts Check whether alerts are compatible with a bookmark configuration: ``` result = ws.validate_alerts_for_bookmark(mp.ValidateAlertsForBookmarkParams( alert_ids=[1, 2, 3], bookmark_type="insights", bookmark_params={"event": "Signup"}, )) if result.invalid_count > 0: for v in result.alert_validations: if not v.valid: print(f"{v.alert_name}: {v.reason}") ``` ``` mp alerts validate \ --alert-ids 1,2,3 \ --bookmark-type insights \ --bookmark-params '{"event": "Signup"}' ``` ______________________________________________________________________ ## Annotations Timeline annotations mark important events (releases, incidents, campaigns) on your Mixpanel charts. ### List Annotations ``` import mixpanel_headless as mp ws = mp.Workspace() # List all annotations annotations = ws.list_annotations() # Filter by date range annotations = ws.list_annotations( from_date="2025-01-01", to_date="2025-03-31", ) # Filter by tags annotations = ws.list_annotations(tags=[1, 2]) for ann in annotations: print(f"{ann.date}: {ann.description}") ``` ``` # List all annotations mp annotations list # Filter by date range mp annotations list --from-date 2025-01-01 --to-date 2025-03-31 # Filter by tags mp annotations list --tags 1,2 ``` ### Create an Annotation Date Format Annotation dates must use `%Y-%m-%d %H:%M:%S` format (e.g., `"2025-03-31 00:00:00"`). ``` annotation = ws.create_annotation(mp.CreateAnnotationParams( date="2025-03-31 00:00:00", description="v2.5 release", tags=[1], # Optional tag IDs )) print(f"Created annotation {annotation.id}") ``` ``` mp annotations create \ --date "2025-03-31 00:00:00" \ --description "v2.5 release" \ --tags 1 ``` ### Get, Update, Delete Immutable Date The annotation date cannot be changed after creation. Only `description` and `tags` are updatable. ``` # Get ann = ws.get_annotation(123) # Update (description and tags only) updated = ws.update_annotation(123, mp.UpdateAnnotationParams( description="v2.5 release (hotfix applied)", )) # Delete ws.delete_annotation(123) ``` ``` # Get mp annotations get 123 # Update mp annotations update 123 --description "v2.5 release (hotfix applied)" # Delete mp annotations delete 123 ``` ### Annotation Tags Organize annotations with tags: ``` # List tags tags = ws.list_annotation_tags() for t in tags: print(f"{t.id}: {t.name}") # Create a tag tag = ws.create_annotation_tag(mp.CreateAnnotationTagParams(name="releases")) ``` ``` # List tags mp annotations tags list # Create a tag mp annotations tags create --name releases ``` ______________________________________________________________________ ## Webhooks Project webhooks receive HTTP notifications when events occur in your Mixpanel project. ### List Webhooks ``` import mixpanel_headless as mp ws = mp.Workspace() webhooks = ws.list_webhooks() for wh in webhooks: print(f"{wh.id}: {wh.name} ({wh.url}) enabled={wh.is_enabled}") ``` ``` mp webhooks list mp webhooks list --format table ``` ### Create a Webhook ``` result = ws.create_webhook(mp.CreateWebhookParams( name="Pipeline webhook", url="https://example.com/webhook", )) print(f"Created webhook {result.id}") # With basic auth result = ws.create_webhook(mp.CreateWebhookParams( name="Authenticated webhook", url="https://example.com/webhook", auth_type=mp.WebhookAuthType.BASIC, username="user", password="secret", )) ``` ``` # Simple webhook mp webhooks create --name "Pipeline webhook" --url https://example.com/webhook # With basic auth mp webhooks create \ --name "Authenticated webhook" \ --url https://example.com/webhook \ --auth-type basic \ --username user \ --password secret ``` ### Update, Delete ``` # Update (PATCH semantics) result = ws.update_webhook("wh-uuid-123", mp.UpdateWebhookParams( name="Updated webhook", is_enabled=False, )) # Delete ws.delete_webhook("wh-uuid-123") ``` ``` # Update mp webhooks update wh-uuid-123 --name "Updated webhook" --disable # Delete mp webhooks delete wh-uuid-123 ``` ### Test Connectivity ``` result = ws.test_webhook(mp.WebhookTestParams( url="https://example.com/webhook", )) if result.success: print(f"Webhook reachable (HTTP {result.status_code})") else: print(f"Webhook failed: {result.message}") ``` ``` mp webhooks test --url https://example.com/webhook ``` ______________________________________________________________________ ## Next Steps - [API Reference β€” Workspace](https://mixpanel.github.io/mixpanel-headless/api/workspace/index.md) β€” Complete method signatures and docstrings - [API Reference β€” Types](https://mixpanel.github.io/mixpanel-headless/api/types/index.md) β€” Dashboard, Bookmark, Cohort, Feature Flag, Experiment, Alert, Annotation, and Webhook type definitions - [CLI Reference](https://mixpanel.github.io/mixpanel-headless/cli/index.md) β€” Full CLI command documentation - [Data Governance Guide](https://mixpanel.github.io/mixpanel-headless/guide/data-governance/index.md) β€” Manage Lexicon definitions, drop filters, custom properties, custom events, and lookup tables Copy markdown # Data Governance Manage Mixpanel data governance programmatically: Lexicon definitions (events, properties, tags), drop filters, custom properties, custom events, lookup tables, schema registry, schema enforcement, data auditing, volume anomalies, and event deletion requests. Full CRUD operations with bulk support. Prerequisites Data governance requires **authentication** β€” service account or OAuth credentials. All data governance operations require a **workspace ID** β€” set via `MP_WORKSPACE_ID` env var, `--workspace` / `-w` CLI flag, `Workspace(workspace=N)`, or `ws.use(workspace=N)`. List available workspaces with `mp workspace list` or `ws.workspaces()`. Read-Only Discovery For read-only Lexicon schema exploration (listing events/properties with descriptions and metadata), see the [Discovery guide β€” Lexicon Schemas](https://mixpanel.github.io/mixpanel-headless/guide/discovery/#lexicon-schemas). This guide covers **write operations**: creating, updating, and deleting definitions. ## Lexicon β€” Event Definitions ### Get Event Definitions ``` import mixpanel_headless as mp ws = mp.Workspace() # Get definitions for specific events defs = ws.get_event_definitions(names=["Signup", "Login"]) for d in defs: print(f"{d.name}: {d.description}") print(f" hidden={d.hidden}, verified={d.verified}") print(f" tags={d.tags}") ``` ``` # Get definitions by name mp lexicon events get --names "Signup,Login" # Table format for quick scanning mp lexicon events get --names "Signup,Login" --format table ``` ### Update an Event Definition ``` definition = ws.update_event_definition( "Signup", mp.UpdateEventDefinitionParams( description="User signed up for an account", verified=True, tags=["core", "acquisition"], ), ) print(f"Updated: {definition.name}") ``` ``` mp lexicon events update --name "Signup" \ --description "User signed up for an account" \ --verified \ --tags "core,acquisition" ``` ### Delete an Event Definition ``` ws.delete_event_definition("OldEvent") ``` ``` mp lexicon events delete --name "OldEvent" ``` ### Bulk Update Event Definitions Update multiple event definitions in a single API call: ``` results = ws.bulk_update_event_definitions( mp.BulkUpdateEventsParams(events=[ mp.BulkEventUpdate(name="OldEvent", hidden=True), mp.BulkEventUpdate(name="NewEvent", verified=True), mp.BulkEventUpdate( name="Purchase", description="Completed purchase", tags=["revenue"], ), ]) ) for d in results: print(f"{d.name}: hidden={d.hidden}, verified={d.verified}") ``` ``` mp lexicon events bulk-update --data '{ "events": [ {"name": "OldEvent", "hidden": true}, {"name": "NewEvent", "verified": true}, {"name": "Purchase", "description": "Completed purchase", "tags": ["revenue"]} ] }' ``` ______________________________________________________________________ ## Lexicon β€” Property Definitions ### Get Property Definitions ``` # Get property definitions by name defs = ws.get_property_definitions( names=["plan_type", "country"], resource_type="event", # "event", "user", or "groupprofile" ) for d in defs: print(f"{d.name} ({d.resource_type}): {d.description}") print(f" sensitive={d.sensitive}, hidden={d.hidden}") ``` ``` # Get property definitions mp lexicon properties get --names "plan_type,country" # Filter by resource type mp lexicon properties get --names "plan_type" --resource-type event ``` ### Update a Property Definition ``` definition = ws.update_property_definition( "plan_type", mp.UpdatePropertyDefinitionParams( description="User subscription tier", sensitive=False, ), ) ``` ``` mp lexicon properties update --name "plan_type" \ --description "User subscription tier" \ --no-sensitive ``` ### Bulk Update Property Definitions ``` results = ws.bulk_update_property_definitions( mp.BulkUpdatePropertiesParams(properties=[ mp.BulkPropertyUpdate( name="email", resource_type="user", sensitive=True, ), mp.BulkPropertyUpdate( name="country", resource_type="event", description="User country code", ), ]) ) ``` ``` mp lexicon properties bulk-update --data '{ "properties": [ {"name": "email", "resource_type": "user", "sensitive": true}, {"name": "country", "resource_type": "event", "description": "User country code"} ] }' ``` ______________________________________________________________________ ## Lexicon β€” Tags Organize event and property definitions with tags. ### List Tags ``` tags = ws.list_lexicon_tags() for tag in tags: print(f"{tag.id}: {tag.name}") ``` ``` mp lexicon tags list mp lexicon tags list --format table ``` ### Create a Tag ``` tag = ws.create_lexicon_tag(mp.CreateTagParams(name="core-events")) print(f"Created tag {tag.id}: {tag.name}") ``` ``` mp lexicon tags create --name "core-events" ``` ### Update a Tag ``` tag = ws.update_lexicon_tag(5, mp.UpdateTagParams(name="renamed-tag")) ``` ``` mp lexicon tags update --id 5 --name "renamed-tag" ``` ### Delete a Tag ``` ws.delete_lexicon_tag("deprecated-tag") ``` ``` mp lexicon tags delete --name "deprecated-tag" ``` ______________________________________________________________________ ## Lexicon β€” Tracking & History ### Tracking Metadata Get tracking metadata for an event (sources, SDKs, volume): ``` metadata = ws.get_tracking_metadata("Signup") print(metadata) # Raw tracking metadata dictionary ``` ``` mp lexicon tracking-metadata --event-name "Signup" ``` ### Event History View the change history for an event definition: ``` history = ws.get_event_history("Signup") for entry in history: print(entry) # Chronological list of changes ``` ``` mp lexicon event-history --event-name "Signup" ``` ### Property History View the change history for a property definition: ``` history = ws.get_property_history("plan_type", entity_type="event") for entry in history: print(entry) ``` ``` mp lexicon property-history --property-name "plan_type" --entity-type "event" ``` ### Export Lexicon Export all Lexicon data definitions: ``` export = ws.export_lexicon() print(export) # Raw export dictionary # Filter by type export = ws.export_lexicon( export_types=["events", "event_properties"] ) ``` ``` mp lexicon export mp lexicon export --types "events,event_properties,user_properties" ``` ______________________________________________________________________ ## Drop Filters Drop filters suppress events at ingestion time, preventing them from being stored or counted. ### List Drop Filters ``` import mixpanel_headless as mp ws = mp.Workspace() filters = ws.list_drop_filters() for f in filters: print(f"{f.id}: {f.event_name} (active={f.active})") ``` ``` mp drop-filters list mp drop-filters list --format table ``` ### Create a Drop Filter ``` filters = ws.create_drop_filter( mp.CreateDropFilterParams( event_name="Debug Event", filters={"property": "env", "value": "test"}, ) ) # Returns full list of drop filters after creation ``` ``` mp drop-filters create --event-name "Debug Event" \ --filters '{"property": "env", "value": "test"}' ``` ### Update a Drop Filter ``` filters = ws.update_drop_filter( mp.UpdateDropFilterParams( id=42, event_name="Debug Event v2", active=False, ) ) ``` ``` mp drop-filters update --id 42 --event-name "Debug Event v2" --no-active ``` ### Delete a Drop Filter ``` remaining = ws.delete_drop_filter(42) # Returns full list of remaining drop filters ``` ``` mp drop-filters delete --id 42 ``` ### Drop Filter Limits ``` limits = ws.get_drop_filter_limits() print(f"Drop filter limit: {limits.filter_limit}") ``` ``` mp drop-filters limits ``` ______________________________________________________________________ ## Custom Properties Custom properties are computed properties defined by formulas or behaviors. They calculate values dynamically from existing event or profile properties. PUT Semantics Custom property `update` uses **full replacement** (PUT semantics). The `resource_type` and `data_group_id` fields are immutable after creation. ### List Custom Properties ``` import mixpanel_headless as mp ws = mp.Workspace() props = ws.list_custom_properties() for p in props: print(f"{p.name}: {p.display_formula}") ``` ``` mp custom-properties list mp custom-properties list --format table ``` ### Get a Custom Property ``` prop = ws.get_custom_property("abc123") print(f"{prop.name}: {prop.display_formula}") print(f" resource_type={prop.resource_type}") ``` ``` mp custom-properties get --id "abc123" ``` ### Create a Custom Property Custom properties can be formula-based or behavior-based: ``` # Formula-based custom property prop = ws.create_custom_property( mp.CreateCustomPropertyParams( name="Full Name", resource_type="events", display_formula='concat(properties["first"], " ", properties["last"])', composed_properties={ "first": mp.ComposedPropertyValue(resource_type="event"), "last": mp.ComposedPropertyValue(resource_type="event"), }, ) ) print(f"Created: {prop.name} (ID: {prop.custom_property_id})") ``` ``` mp custom-properties create \ --name "Full Name" \ --resource-type events \ --display-formula 'concat(properties["first"], " ", properties["last"])' \ --composed-properties '{"first": {"resource_type": "event"}, "last": {"resource_type": "event"}}' ``` Formula vs Behavior `display_formula` and `behavior` are mutually exclusive. If using `display_formula`, you must also provide `composed_properties` that map the referenced properties. ### Update a Custom Property ``` prop = ws.update_custom_property( "abc123", mp.UpdateCustomPropertyParams(name="Renamed Property"), ) ``` ``` mp custom-properties update --id "abc123" --name "Renamed Property" ``` ### Delete a Custom Property ``` ws.delete_custom_property("abc123") ``` ``` mp custom-properties delete --id "abc123" ``` ### Validate a Custom Property Check whether a custom property definition is valid before creating it: ``` result = ws.validate_custom_property( mp.CreateCustomPropertyParams( name="Revenue Per User", resource_type="events", display_formula='number(properties["amount"])', composed_properties={ "amount": mp.ComposedPropertyValue(resource_type="event"), }, ) ) print(result) # Validation result dictionary ``` ``` mp custom-properties validate \ --name "Revenue Per User" \ --resource-type events \ --display-formula 'number(properties["amount"])' \ --composed-properties '{"amount": {"resource_type": "event"}}' ``` ______________________________________________________________________ ## Custom Events Custom events are composite aliases that group one or more underlying events under a single display name. They appear alongside regular events in queries and dashboards but resolve to the union of their underlying events at query time. The `/custom_events/` endpoints (used by `create_custom_event`) return the typed `CustomEvent` model β€” distinct from `EventDefinition`, which is the lexicon (governance) view returned by `list_custom_events`, `update_custom_event`, and the rest of the `/data-definitions/events/` family. ### List Custom Events ``` import mixpanel_headless as mp ws = mp.Workspace() events = ws.list_custom_events() for e in events: print(f"{e.name}: {e.description}") ``` ``` mp custom-events list mp custom-events list --format table ``` ### Create a Custom Event Create a new custom event by giving it a display name and the list of underlying event names it should alias. The `alternatives` field accepts bare event names (strings) and is serialized internally to the format the Mixpanel API expects. ``` import mixpanel_headless as mp ws = mp.Workspace() ce = ws.create_custom_event( mp.CreateCustomEventParams( name="Page View", alternatives=["Home Viewed", "Product Viewed", "Checkout Viewed"], ) ) print(ce.id, ce.name, [a.event for a in ce.alternatives]) ``` ``` mp custom-events create --name "Page View" \ --alternative "Home Viewed" \ --alternative "Product Viewed" \ --alternative "Checkout Viewed" ``` The CLI's `--alternative` option is repeatable β€” pass it once per underlying event. ### Update a Custom Event Identify the custom event by its `custom_event_id` (returned by `create_custom_event` or available on entries from `list_custom_events`). Updating by name alone would create an orphan lexicon entry β€” the Mixpanel API needs the id to disambiguate. ``` ce = ws.create_custom_event( mp.CreateCustomEventParams(name="My Custom Event", alternatives=["Foo"]) ) event = ws.update_custom_event( ce.id, mp.UpdateEventDefinitionParams( description="Updated description", verified=True, ), ) ``` ``` # Preferred: target by ID mp custom-events update --id 2044168 \ --description "Updated description" --verified # Convenience: target by name (errors if name is ambiguous) mp custom-events update --name "My Custom Event" \ --description "Updated description" --verified ``` ### Delete a Custom Event Identify the custom event by its `custom_event_id` for the same reason as `update_custom_event` β€” a name-only DELETE against the data-definitions endpoint is ambiguous when display names collide and may delete the wrong row, an orphan lexicon entry, or no-op silently while reporting success. ``` ws.delete_custom_event(2044168) ``` ``` # Preferred: target by ID mp custom-events delete --id 2044168 # Convenience: target by name (errors if name is ambiguous) mp custom-events delete --name "My Custom Event" ``` ______________________________________________________________________ ## Lookup Tables Lookup tables are CSV-based reference data used to enrich event and profile properties. Upload a CSV, and Mixpanel maps its columns to properties for real-time enrichment. ### List Lookup Tables ``` import mixpanel_headless as mp ws = mp.Workspace() tables = ws.list_lookup_tables() for t in tables: print(f"{t.name} (ID: {t.id}, mapped={t.has_mapped_properties})") # Filter by data group tables = ws.list_lookup_tables(data_group_id=123) ``` ``` mp lookup-tables list mp lookup-tables list --data-group-id 123 mp lookup-tables list --format table ``` ### Upload a Lookup Table 3-Step Upload Process `upload_lookup_table()` handles the full workflow automatically: 1. Obtains a signed upload URL from the API 1. Uploads the CSV file to the signed URL 1. Registers the lookup table For files >= 5 MB, processing is asynchronous. The method automatically polls for completion with configurable timeout. ``` table = ws.upload_lookup_table( mp.UploadLookupTableParams( name="Country Codes", file_path="/path/to/countries.csv", ), poll_interval=2.0, # Seconds between polls (async only) max_poll_seconds=300.0, # Max wait time (async only) ) print(f"Created: {table.name} (ID: {table.id})") # Replace an existing lookup table table = ws.upload_lookup_table( mp.UploadLookupTableParams( name="Country Codes", file_path="/path/to/countries_v2.csv", data_group_id=456, # Existing table's data group ID ) ) ``` ``` # Upload a new lookup table mp lookup-tables upload --name "Country Codes" --file "/path/to/countries.csv" # Replace an existing lookup table mp lookup-tables upload --name "Country Codes" --file "/path/to/countries_v2.csv" \ --data-group-id 456 ``` ### Update a Lookup Table ``` table = ws.update_lookup_table( data_group_id=123, params=mp.UpdateLookupTableParams(name="Renamed Table"), ) ``` ``` mp lookup-tables update --data-group-id 123 --name "Renamed Table" ``` ### Delete Lookup Tables ``` # Delete one or more lookup tables ws.delete_lookup_tables(data_group_ids=[123, 456]) ``` ``` mp lookup-tables delete --data-group-ids "123,456" ``` ### Download a Lookup Table ``` # Download as raw CSV bytes csv_data = ws.download_lookup_table(data_group_id=123) # Save to file with open("output.csv", "wb") as f: f.write(csv_data) # Download with row limit csv_data = ws.download_lookup_table(data_group_id=123, limit=100) ``` ``` # Download to stdout mp lookup-tables download --data-group-id 123 # Save to file mp lookup-tables download --data-group-id 123 > output.csv ``` ### Advanced: Upload and Download URLs For manual upload workflows or integration with external tools: ``` # Get a signed upload URL url_info = ws.get_lookup_upload_url(content_type="text/csv") print(url_info.url) # Signed GCS URL # Check async upload status status = ws.get_lookup_upload_status("upload-id-123") print(status) # Get a signed download URL download_url = ws.get_lookup_download_url(data_group_id=123) print(download_url) ``` ``` # Get upload URL mp lookup-tables upload-url # Get download URL mp lookup-tables download-url --data-group-id 123 ``` ______________________________________________________________________ ## Schema Registry Manage JSON Schema Draft 7 definitions in Mixpanel's schema registry. Schemas define the expected structure of events, custom events, and profiles. ### List Schema Entries ``` import mixpanel_headless as mp ws = mp.Workspace() # List all schemas schemas = ws.list_schema_registry() for s in schemas: print(f"{s.entity_type}/{s.name}: v{s.version}") # Filter by entity type event_schemas = ws.list_schema_registry(entity_type="event") ``` ``` mp schemas list mp schemas list --entity-type event mp schemas list --format table ``` ### Create a Schema ``` result = ws.create_schema( entity_type="event", entity_name="Purchase", schema_json={ "properties": { "amount": {"type": "number"}, "currency": {"type": "string"}, }, "required": ["amount"], }, ) ``` ``` mp schemas create --entity-type event --entity-name "Purchase" \ --schema-json '{"properties": {"amount": {"type": "number"}}}' ``` ### Bulk Create Schemas ``` params = mp.BulkCreateSchemasParams( entries=[ mp.SchemaEntry( name="Login", entity_type="event", schema_definition={"properties": {"method": {"type": "string"}}}, ), mp.SchemaEntry( name="Signup", entity_type="event", schema_definition={"properties": {"source": {"type": "string"}}}, ), ], truncate=False, entity_type="event", ) result = ws.create_schemas_bulk(params) print(f"Added: {result.added}, Deleted: {result.deleted}") ``` ``` mp schemas create-bulk \ --entries-json '[{"name": "Login", "entityType": "event", "schemaDefinition": {"properties": {"method": {"type": "string"}}}}]' # With truncate (replaces all existing schemas of this type) mp schemas create-bulk --entries-json '[...]' --truncate ``` ### Update a Schema (Merge Semantics) ``` result = ws.update_schema( entity_type="event", entity_name="Purchase", schema_json={ "properties": { "discount_code": {"type": "string"}, }, }, ) ``` ``` mp schemas update --entity-type event --entity-name "Purchase" \ --schema-json '{"properties": {"discount_code": {"type": "string"}}}' ``` ### Bulk Update Schemas ``` params = mp.BulkCreateSchemasParams( entries=[ mp.SchemaEntry( name="Login", entity_type="event", schema_definition={"properties": {"ip_address": {"type": "string"}}}, ), ], entity_type="event", ) results = ws.update_schemas_bulk(params) for r in results: print(f"{r.name}: {r.status}") ``` ``` mp schemas update-bulk --entries-json '[{"name": "Login", "entityType": "event", "schemaDefinition": {...}}]' ``` ### Delete Schemas Destructive Operation Schema deletion is irreversible. The CLI prompts for confirmation before proceeding. ``` # Delete a specific schema result = ws.delete_schemas(entity_type="event", entity_name="Purchase") print(f"Deleted: {result.delete_count}") # Delete all schemas of a type result = ws.delete_schemas(entity_type="event") ``` ``` mp schemas delete --entity-type event --entity-name "Purchase" mp schemas delete --entity-type event ``` ______________________________________________________________________ ## Schema Enforcement Configure how Mixpanel handles events that don't match defined schemas. Enforcement actions include "Warn and Accept", "Warn and Hide", and "Warn and Drop". ### Get Enforcement Settings ``` config = ws.get_schema_enforcement() print(f"State: {config.state}") print(f"Rule: {config.rule_event}") # Get specific fields only config = ws.get_schema_enforcement(fields="state,ruleEvent") ``` ``` mp lexicon enforcement get mp lexicon enforcement get --fields "state,ruleEvent" mp lexicon enforcement get --format table ``` ### Initialize Enforcement ``` result = ws.init_schema_enforcement( mp.InitSchemaEnforcementParams(rule_event="Warn and Accept"), ) ``` ``` mp lexicon enforcement init --rule-event "Warn and Accept" ``` ### Update Enforcement (PATCH) ``` result = ws.update_schema_enforcement( mp.UpdateSchemaEnforcementParams( rule_event="Warn and Drop", notification_emails=["data-team@example.com"], ), ) ``` ``` mp lexicon enforcement update \ --params-json '{"ruleEvent": "Warn and Drop", "notificationEmails": ["data-team@example.com"]}' ``` ### Replace Enforcement (PUT) Full Replacement PUT semantics replace the entire enforcement configuration. All fields must be provided. The CLI prompts for confirmation. ``` result = ws.replace_schema_enforcement( mp.ReplaceSchemaEnforcementParams( events=[], common_properties=[], user_properties=[], rule_event="Warn and Hide", notification_emails=["admin@example.com"], ), ) ``` ``` mp lexicon enforcement replace \ --params-json '{"events": [], "commonProperties": [], "userProperties": [], "ruleEvent": "Warn and Hide", "notificationEmails": ["admin@example.com"]}' ``` ### Delete Enforcement Destructive Operation Deleting enforcement configuration is irreversible. The CLI prompts for confirmation. ``` result = ws.delete_schema_enforcement() ``` ``` mp lexicon enforcement delete ``` ______________________________________________________________________ ## Data Auditing Audit your project's data against defined schemas to find violations such as unexpected events, missing properties, or type mismatches. ### Run Full Audit ``` audit = ws.run_audit() print(f"Computed at: {audit.computed_at}") for v in audit.violations: print(f" [{v.violation_type}] {v.event_name}: {v.description}") ``` ``` mp lexicon audit mp lexicon audit --format table ``` ### Run Events-Only Audit A faster variant that only audits event schemas, skipping property-level checks. ``` audit = ws.run_audit_events_only() for v in audit.violations: print(f" {v.event_name}: {v.description}") ``` ``` mp lexicon audit --events-only ``` ______________________________________________________________________ ## Data Volume Anomalies Monitor and manage anomalies detected in data volume patterns. Anomalies indicate unexpected spikes or drops that may signal tracking issues or data pipeline problems. ### List Anomalies ``` # List all anomalies anomalies = ws.list_data_volume_anomalies() for a in anomalies: print(f"{a.event_name}: {a.status} (variance: {a.variance})") # Filter by status open_anomalies = ws.list_data_volume_anomalies( query_params={"status": "open"}, ) ``` ``` mp lexicon anomalies list mp lexicon anomalies list --status open mp lexicon anomalies list --event-name "Purchase" --format table ``` ### Update an Anomaly ``` result = ws.update_anomaly( mp.UpdateAnomalyParams( id=123, status="dismissed", anomaly_class="Event", ), ) ``` ``` mp lexicon anomalies update --id 123 --status dismissed --anomaly-class Event ``` ### Bulk Update Anomalies ``` result = ws.bulk_update_anomalies( mp.BulkUpdateAnomalyParams( anomalies=[ mp.BulkAnomalyEntry(id=1, anomaly_class="Event"), mp.BulkAnomalyEntry(id=2, anomaly_class="Event"), ], status="dismissed", ), ) ``` ``` mp lexicon anomalies bulk-update \ --params-json '{"anomalies": [{"id": 1, "anomalyClass": "Event"}, {"id": 2, "anomalyClass": "Event"}], "status": "dismissed"}' ``` ______________________________________________________________________ ## Event Deletion Requests Submit and manage requests to delete event data by event name, date range, and optional property filters. Destructive Operation Event deletion is irreversible. Use `preview` to validate filters before creating a deletion request. ### List Deletion Requests ``` requests = ws.list_deletion_requests() for r in requests: print(f"#{r.id}: {r.event_name} ({r.status})") ``` ``` mp lexicon deletion-requests list mp lexicon deletion-requests list --format table ``` ### Preview Deletion Filters Preview what events would be affected before submitting a deletion request. This is a read-only operation with no side effects. ``` preview = ws.preview_deletion_filters( mp.PreviewDeletionFiltersParams( event_name="Test Event", from_date="2026-01-01", to_date="2026-01-31", ), ) for item in preview: print(item) ``` ``` mp lexicon deletion-requests preview \ --event-name "Test Event" \ --from-date 2026-01-01 --to-date 2026-01-31 ``` ### Create a Deletion Request ``` result = ws.create_deletion_request( mp.CreateDeletionRequestParams( event_name="Test Event", from_date="2026-01-01", to_date="2026-01-31", ), ) # Returns updated list of all deletion requests for r in result: print(f"#{r.id}: {r.status}") ``` ``` mp lexicon deletion-requests create \ --event-name "Test Event" \ --from-date 2026-01-01 --to-date 2026-01-31 ``` ### Cancel a Deletion Request Only pending requests can be cancelled. The CLI prompts for confirmation. ``` result = ws.cancel_deletion_request(request_id=456) ``` ``` mp lexicon deletion-requests cancel --id 456 ``` ______________________________________________________________________ ## Next Steps - [API Reference β€” Workspace](https://mixpanel.github.io/mixpanel-headless/api/workspace/index.md) β€” Complete method signatures and docstrings - [API Reference β€” Types](https://mixpanel.github.io/mixpanel-headless/api/types/index.md) β€” EventDefinition, DropFilter, CustomProperty, LookupTable, SchemaEntry, SchemaEnforcementConfig, AuditResponse, DataVolumeAnomaly, EventDeletionRequest, and all parameter types - [CLI Reference](https://mixpanel.github.io/mixpanel-headless/cli/index.md) β€” Full CLI command documentation - [Entity Management](https://mixpanel.github.io/mixpanel-headless/guide/entity-management/index.md) β€” Manage dashboards, reports, cohorts, feature flags, experiments, alerts, annotations, and webhooks Copy markdown # Business Context Read and write the markdown documentation that grounds AI assistants in your organization's structure and goals, exposed as a Python API and `mp business-context` CLI group. What is Business Context? Business Context is plain markdown text (up to **50,000 characters per scope**) that you attach to a Mixpanel organization or project. AI assistants read it before answering questions, so they know *what your product does*, *what your events mean*, *which dashboards are canonical*, and *how your team defines key metrics*. See the [official Mixpanel Business Context docs](https://docs.mixpanel.com/docs/business-context) for the broader product picture. Prerequisites Business Context requires **authentication**. Project-level reads work with any account that has project access; project-level writes additionally require `edit_project_info` permission. Org-level operations require org membership (read) plus `edit_project_info` at the org level (write). Service accounts can read/write at the project level for projects they're attached to; for org-level operations or for org-id auto-resolution, an OAuth account (`oauth_browser` or `oauth_token`) is the cleanest path. ## Two scopes | Scope | Lives at | Shared by | | -------------- | ------------------------- | ------------------------ | | `organization` | The Mixpanel organization | Every project in the org | | `project` | A single project | That project only | `mixpanel_headless` exposes both scopes through the same API, gated by a `level: Literal["organization", "project"]` argument. ## Quick reference ``` import mixpanel_headless as mp ws = mp.Workspace() # Read project_ctx = ws.get_business_context(level="project") org_ctx = ws.get_business_context(level="organization") # Read both at once (single round-trip via /business-context/chain) chain = ws.get_business_context_chain() # Write ws.set_business_context("# About Acme\n…", level="project") ws.set_business_context("# Org-wide context", level="organization") # Clear (equivalent to set_business_context("")) ws.clear_business_context(level="project") ``` ``` # Read mp business-context get --level project mp business-context get --level organization mp business-context chain # both at once # Write β€” three input modes mp business-context set --level project --content "# About Acme..." mp business-context set --level project --file context.md cat context.md | mp business-context set --level project # Clear mp business-context clear --level project ``` ## Reading context ### Project scope Project-scope reads use the active session's project ID. If no context has been set, the API returns the empty string β€” no special "not found" error to handle. ``` ctx = ws.get_business_context(level="project") print(f"{ctx.character_count} chars") if ctx.is_empty: print("No project context configured.") else: print(ctx.content) ``` ``` # Pretty-printed JSON (default) mp business-context get --level project # Just the markdown body via jq mp business-context get --level project --jq '.content' # Compact rich table mp business-context get --level project --format table ``` ### Organization scope Organization-scope reads default to the org that owns the active session's project. The org ID is auto-resolved from the cached `/me` response (24-hour TTL). To read context from a different org without switching projects, pass `organization_id` explicitly. ``` # Auto-resolve org_id from the active project's organization org_ctx = ws.get_business_context(level="organization") print(f"org={org_ctx.organization_id}: {org_ctx.character_count} chars") # Explicit override (skips the /me lookup) other = ws.get_business_context(level="organization", organization_id=42) ``` ``` # Auto-resolve from active project mp business-context get --level organization # Explicit org id mp business-context get --level organization --organization-id 42 ``` If auto-resolution can't determine the org (e.g. the active project isn't in the cached `/me` and the user belongs to multiple orgs), the call raises `WorkspaceScopeError` with `code="ORGANIZATION_AMBIGUOUS"` and lists the accessible org IDs. ### Both scopes in one call The server exposes a `/business-context/chain` endpoint that returns both org and project context together, scoped to the active project. Use `get_business_context_chain()` (Python) or `mp business-context chain` (CLI) to avoid two round-trips. `organization.organization_id` on the returned chain is populated **best-effort** from the cached `/me` response (in-memory or disk). When the cache is cold the field is left as `None` β€” the chain endpoint deliberately does *not* trigger an extra `/me` fetch, preserving its single-network-round-trip property. Callers that need a guaranteed org ID should call `get_business_context(level="organization")`, which performs full resolution. ``` chain = ws.get_business_context_chain() print("ORG: ", chain.organization.content) print("PROJECT:", chain.project.content) ``` ``` mp business-context chain mp business-context chain --jq '.project.content' ``` ## Writing context `set_business_context` is **full-replace** semantics β€” what you pass becomes the entire stored content for that scope. There is no append, no patch, no diff. Pass the empty string to clear, or use `clear_business_context` for clarity. ``` new_content = """# Acme Analytics ## Product overview Acme is a SaaS dashboard for SMBs. ## Event taxonomy - `signup_completed` β€” user creates an account - `subscription_started` β€” paid plan begins - `feature_X_used` β€” pattern for feature engagement ## Definitions - **Active user**: any user with β‰₯1 event in the last 28 days """ ws.set_business_context(new_content, level="project") ws.set_business_context("# Org-wide standards…", level="organization") ws.set_business_context("", level="project") # clear ws.clear_business_context(level="project") # same thing, more explicit ``` The `set` command accepts content from three sources, in priority order: 1. `--content TEXT` β€” inline markdown (best for short content; pass `""` to clear) 1. `--file PATH` β€” read from a file on disk 1. **stdin** β€” when no flags are given and stdin is not a TTY `--content` and `--file` are mutually exclusive. Stdin is only consulted when neither flag is provided. **Empty / whitespace-only stdin is rejected** (exit code 3) β€” use `mp business-context clear` to deliberately clear, so a CI/cron run with ` {e.details['max']}") ``` ``` # Oversize content from a file mp business-context set --level project --file too_big.md # β†’ exits with code 3 (INVALID_ARGS) and a clear message, # without making a network request ``` ### Clearing `clear_business_context()` is a thin convenience over `set_business_context("")`. Use whichever reads better at the call site. ``` ws.clear_business_context(level="project") ws.clear_business_context(level="organization") ``` ``` mp business-context clear --level project mp business-context clear --level organization ``` ## Common workflows ### Version-control project context as a file Treat `context.md` like any other source file in your repo, and re-apply it as part of deploy: ``` # In CI or a deploy hook mp business-context set --level project --file ./context.md ``` ### Bootstrap a new project from the org default ``` import mixpanel_headless as mp ws = mp.Workspace() chain = ws.get_business_context_chain() # Seed the project with the org content (e.g. for a new project that should # inherit org standards as a starting point) if chain.project.is_empty and not chain.organization.is_empty: ws.set_business_context(chain.organization.content, level="project") ``` ### Audit context across many projects ``` import mixpanel_headless as mp ws = mp.Workspace() for project in ws.projects(): ws.use(project=project.id) ctx = ws.get_business_context(level="project") if ctx.is_empty: print(f"⚠️ {project.id} ({project.name}) has no project context") else: print(f"βœ… {project.id} ({project.name}) β€” {ctx.character_count} chars") ``` ## Result types `get_business_context` and `set_business_context` return [`BusinessContext`](https://mixpanel.github.io/mixpanel-headless/api/types/#mixpanel_headless.BusinessContext) β€” a frozen Pydantic model with the markdown content plus the scope-appropriate identifier: | Field | Project scope | Org scope | | ----------------- | ----------------------- | ----------------------- | | `level` | `"project"` | `"organization"` | | `content` | markdown body (or `""`) | markdown body (or `""`) | | `project_id` | active project ID | `None` | | `organization_id` | `None` | resolved org ID | Two computed fields are also exposed and **appear in `model_dump()`** (so `--jq '.is_empty'` and `--jq '.character_count'` work directly from the CLI): - `is_empty: bool` β€” `True` when `content == ""` - `character_count: int` β€” `len(content)`; compare against `BUSINESS_CONTEXT_MAX_CHARS` `get_business_context_chain()` returns [`BusinessContextChain`](https://mixpanel.github.io/mixpanel-headless/api/types/#mixpanel_headless.BusinessContextChain), which is just `{organization: BusinessContext, project: BusinessContext}`. ## Error handling | Exception | Raised when | | ------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | | [`BusinessContextValidationError`](https://mixpanel.github.io/mixpanel-headless/api/exceptions/#mixpanel_headless.BusinessContextValidationError) | Client-side: content > 50,000 chars (no HTTP call made) | | [`QueryError`](https://mixpanel.github.io/mixpanel-headless/api/exceptions/#mixpanel_headless.QueryError) | Server-side 400 (malformed body, server-side oversize), 403 (missing `edit_project_info`), 404 (org/project not visible) | | [`AuthenticationError`](https://mixpanel.github.io/mixpanel-headless/api/exceptions/#mixpanel_headless.AuthenticationError) | 401 β€” credentials are invalid | | [`WorkspaceScopeError`](https://mixpanel.github.io/mixpanel-headless/api/exceptions/#mixpanel_headless.WorkspaceScopeError) | `level="organization"` and the org ID could not be auto-resolved (`code="ORGANIZATION_AMBIGUOUS"`) | | [`ServerError`](https://mixpanel.github.io/mixpanel-headless/api/exceptions/#mixpanel_headless.ServerError) | 5xx | ``` import mixpanel_headless as mp ws = mp.Workspace() try: ws.set_business_context("…", level="organization") except mp.BusinessContextValidationError as e: print(f"Too long ({e.details['length']} chars), max {e.details['max']}") except mp.WorkspaceScopeError as e: print(f"Cannot resolve org: {e.message}") except mp.QueryError as e: print(f"API rejected the write: {e.status_code} {e.message}") ``` ## Permissions summary | Operation | Required permission | | ------------------- | ----------------------------------------------------- | | Project-level read | Project access (read) | | Project-level write | Project access + `edit_project_info` | | Org-level read | Org membership | | Org-level write | Org membership + `edit_project_info` at the org level | Service accounts attached to a project can read and write that project's context. Org-level writes typically require an OAuth account (`oauth_browser` or `oauth_token`) whose principal has org-level edit permissions. The 50,000-character cap is enforced both client-side (before any HTTP call) and server-side. ## Next steps - Reference: [Workspace API](https://mixpanel.github.io/mixpanel-headless/api/workspace/index.md) for the full method surface. - Reference: [Result Types β€” Business Context](https://mixpanel.github.io/mixpanel-headless/api/types/#business-context-types) and [Exceptions β€” Business Context](https://mixpanel.github.io/mixpanel-headless/api/exceptions/#business-context-exceptions). - CLI: [`mp business-context` command reference](https://mixpanel.github.io/mixpanel-headless/cli/commands/index.md) (auto-generated from the Typer app). - Product docs: the [Mixpanel Business Context product page](https://docs.mixpanel.com/docs/business-context) for organization-level rollout guidance. Copy markdown # API Reference # API Overview The `mixpanel_headless` Python API provides programmatic access to all library functionality. Explore on DeepWiki πŸ€– **[Python API Reference β†’](https://deepwiki.com/mixpanel/mixpanel-headless/7.2-python-api-reference)** Ask questions about API methods, explore usage patterns, or get help with specific functionality. ## Import Patterns ``` # Recommended: import with alias import mixpanel_headless as mp ws = mp.Workspace() result = ws.query("Login", math="unique", last=30) # Direct imports from mixpanel_headless import Workspace, MixpanelHeadlessError # Insights Query types from mixpanel_headless import ( Metric, Filter, Formula, GroupBy, QueryResult, MathType, PerUserAggregation, FilterPropertyType, ) # Cohort Query types (cross-engine) from mixpanel_headless import ( CohortBreakdown, CohortMetric, CohortDefinition, CohortCriteria, ) # Advanced Query types (cross-engine) from mixpanel_headless import TimeComparison, FrequencyBreakdown, FrequencyFilter # Retention Query types from mixpanel_headless import RetentionEvent, RetentionQueryResult # Flow Query types from mixpanel_headless import FlowStep, FlowTreeNode, FlowQueryResult # User Profile Query types from mixpanel_headless import UserQueryResult # Auth surface β€” recommended top-level imports from mixpanel_headless import ( Account, ServiceAccount, OAuthBrowserAccount, OAuthTokenAccount, Session, Project, WorkspaceRef, Region, AccountSummary, AccountTestResult, OAuthLoginResult, Target, ) # Account/Session/WorkspaceRef/account variants are also available from # mixpanel_headless.auth_types (single source of truth for the auth subsystem); # the top-level form is canonical throughout the docs. # Low-level types live only under mixpanel_headless.auth_types from mixpanel_headless.auth_types import ( OAuthTokens, OAuthClientInfo, TokenResolver, OnDiskTokenResolver, BridgeFile, load_bridge, ActiveSession, ) # Functional namespaces from mixpanel_headless import accounts, session, targets # OAuth and workspace exceptions from mixpanel_headless import OAuthError, WorkspaceScopeError # App API types from mixpanel_headless import PublicWorkspace, CursorPagination, PaginatedResponse # Entity CRUD types from mixpanel_headless import ( Dashboard, CreateDashboardParams, UpdateDashboardParams, Bookmark, CreateBookmarkParams, UpdateBookmarkParams, Cohort, CreateCohortParams, UpdateCohortParams, ) # Data governance types from mixpanel_headless import ( EventDefinition, UpdateEventDefinitionParams, DropFilter, CreateDropFilterParams, CustomProperty, CreateCustomPropertyParams, LookupTable, UploadLookupTableParams, ) # Schema governance types from mixpanel_headless import ( SchemaEntry, BulkCreateSchemasParams, SchemaEnforcementConfig, AuditResponse, DataVolumeAnomaly, EventDeletionRequest, ) ``` ## Core Components ### Workspace The main entry point for all operations: - **Discovery** β€” Explore events, properties, funnels, cohorts - **Insights Queries** β€” Typed analytics queries using the Insights engine (`query()`) - **Funnel Queries** β€” Typed funnel conversion analysis (`query_funnel()`) - **Retention Queries** β€” Typed retention analysis with event pairs (`query_retention()`) - **Flow Queries** β€” Typed flow path analysis (`query_flow()`) - **User Profile Queries** β€” Typed user profile queries with filtering, sorting, and aggregation (`query_user()`) - **Live Queries** β€” Legacy analytics endpoints (segmentation, funnels, retention, JQL) - **Streaming** β€” Stream events and profiles directly from Mixpanel (ETL, pipelines) - **Entity CRUD & Data Governance** β€” Create, read, update, delete dashboards, reports, cohorts, feature flags, experiments, plus Lexicon definitions, drop filters, custom properties, custom events, lookup tables, schema registry, schema enforcement, data auditing, volume anomalies, and event deletion requests [View Workspace API](https://mixpanel.github.io/mixpanel-headless/api/workspace/index.md) ### Auth Surface Three first-class account types and three functional namespaces. Most are re-exported from `mixpanel_headless`; a few low-level types live under `mixpanel_headless.auth_types`: - **`Account`** β€” Discriminated union over `ServiceAccount` (Basic Auth), `OAuthBrowserAccount` (PKCE flow, auto-refreshed), `OAuthTokenAccount` (static bearer for CI/agents) - **`Session` / `Project` / `WorkspaceRef`** β€” Immutable resolved-state types (top-level) - **`ActiveSession`** β€” Persisted `[active]`-block snapshot (only from `mixpanel_headless.auth_types`) - **`mp.accounts`** β€” Account lifecycle: `add`, `list`, `use`, `show`, `test`, `login`, `logout`, `token`, `export_bridge`, `remove_bridge`, `update`, `remove` - **`mp.session`** β€” Read/write the persisted `[active]` block: `show`, `use` - **`mp.targets`** β€” Saved (account, project, optional workspace) cursor positions: `list`, `add`, `use`, `show`, `remove` - **`AccountSummary` / `AccountTestResult` / `OAuthLoginResult` / `Target`** β€” Result types - **`OAuthTokens`** β€” Low-level token type, available from `mixpanel_headless.auth_types` - **`BridgeFile` / `load_bridge`** β€” Cowork bridge v2 integration, available from `mixpanel_headless.auth_types` [View Auth API](https://mixpanel.github.io/mixpanel-headless/api/auth/index.md) ### Exceptions Structured error handling: - **MixpanelHeadlessError** β€” Base exception - **APIError** β€” HTTP/API errors - **ConfigError** β€” Configuration errors - **OAuthError** β€” OAuth authentication errors - **WorkspaceScopeError** β€” Workspace resolution errors [View Exceptions](https://mixpanel.github.io/mixpanel-headless/api/exceptions/index.md) ### Result Types Typed results for all operations: - **QueryResult** β€” Insights query results (from `query()`) - **Metric**, **Filter**, **Formula**, **GroupBy** β€” Query building blocks - **CohortBreakdown**, **CohortMetric** β€” Cohort-scoped query types (cross-engine) - **TimeComparison**, **FrequencyBreakdown**, **FrequencyFilter** β€” Advanced cross-engine query types - **CohortDefinition**, **CohortCriteria** β€” Inline cohort definition builder - **FunnelQueryResult**, **FunnelStep**, **Exclusion** β€” Typed funnel results - **RetentionQueryResult**, **RetentionEvent**, **RetentionAlignment**, **RetentionMode**, **RetentionMathType** β€” Typed retention results - **FlowQueryResult**, **FlowStep**, **FlowTreeNode** β€” Typed flow analysis results - **UserQueryResult** β€” Typed user profile query results - **SegmentationResult** β€” Time-series data (legacy) - **FunnelResult** β€” Funnel conversion data (legacy) - **RetentionResult** β€” Retention cohort data (legacy) - **Dashboard**, **Bookmark**, **Cohort** β€” Entity models for CRUD operations - **EventDefinition**, **DropFilter**, **CustomProperty**, **LookupTable** β€” Data governance models - **SchemaEntry**, **SchemaEnforcementConfig**, **AuditResponse**, **DataVolumeAnomaly**, **EventDeletionRequest** β€” Schema governance models - And many more... [View Result Types](https://mixpanel.github.io/mixpanel-headless/api/types/index.md) ## Type Aliases The library exports these type aliases: ``` from mixpanel_headless import CountType, HourDayUnit, TimeUnit, FilterDateUnit from mixpanel_headless import FilterOperator, FlowCountType, FlowChartType # CountType: Literal["general", "unique", "average", "median", "min", "max"] # HourDayUnit: Literal["hour", "day"] # TimeUnit: Literal["day", "week", "month", "quarter", "year"] # FilterDateUnit: Literal["hour", "day", "week", "month"] # FilterOperator: Literal[26 operator strings β€” see _literal_types.py] # FlowCountType: Literal["unique", "total", "session"] # FlowChartType: Literal["sankey", "paths", "tree"] ``` ## Complete API Reference - [Workspace](https://mixpanel.github.io/mixpanel-headless/api/workspace/index.md) β€” Main facade class - [Auth](https://mixpanel.github.io/mixpanel-headless/api/auth/index.md) β€” Authentication and configuration - [Exceptions](https://mixpanel.github.io/mixpanel-headless/api/exceptions/index.md) β€” Error handling - [Result Types](https://mixpanel.github.io/mixpanel-headless/api/types/index.md) β€” All result dataclasses Copy markdown # Workspace The `Workspace` class is the unified entry point for all Mixpanel data operations. Explore on DeepWiki πŸ€– **[Workspace Class Deep Dive β†’](https://deepwiki.com/mixpanel/mixpanel-headless/3.2.1-workspace-class)** Ask questions about Workspace methods, explore usage patterns, or understand how services are orchestrated. ## Overview Workspace orchestrates internal services and provides direct App API access: - **DiscoveryService** β€” Schema exploration (events, properties, funnels, cohorts) - **LiveQueryService** β€” Real-time analytics queries (legacy) and Insights engine queries - **Streaming** β€” Stream events and profiles directly from Mixpanel - **Entity CRUD** β€” Create, read, update, delete dashboards, reports, and cohorts via Mixpanel App API (workspace-scoped) - **Feature Management** β€” Create, read, update, delete feature flags and experiments via Mixpanel App API (project-scoped) - **Operational Tooling** β€” Manage alerts, annotations, and webhooks via Mixpanel App API (workspace-scoped) - **Data Governance** β€” Manage Lexicon definitions, drop filters, custom properties, custom events, lookup tables, schema registry, schema enforcement, data auditing, volume anomalies, and event deletion requests via Mixpanel App API (workspace-scoped) - **Business Context** β€” Read and write the markdown documentation that grounds AI assistants (org and project scopes, 50,000-char cap) ## Key Features ### Entity CRUD Manage dashboards, reports (bookmarks), and cohorts programmatically via the Mixpanel App API (workspace-scoped): ``` import mixpanel_headless as mp ws = mp.Workspace() # Dashboards dashboards = ws.list_dashboards() new_dash = ws.create_dashboard(mp.CreateDashboardParams(title="Q1 Metrics")) ws.update_dashboard(new_dash.id, mp.UpdateDashboardParams(title="Q1 Metrics v2")) ws.favorite_dashboard(new_dash.id) # Reports (Bookmarks) reports = ws.list_bookmarks_v2() report = ws.create_bookmark(mp.CreateBookmarkParams( name="Daily Signups", bookmark_type="insights" )) # Cohorts cohorts = ws.list_cohorts_full() cohort = ws.create_cohort(mp.CreateCohortParams(name="Power Users")) ws.update_cohort(cohort.id, mp.UpdateCohortParams(name="Super Users")) ``` Dashboard, report, and cohort operations require a workspace ID, set via `MP_WORKSPACE_ID` environment variable, `--workspace` / `-w` CLI flag, `Workspace(workspace=N)`, or `ws.use(workspace=N)`. List available workspaces with `mp workspace list` or `ws.workspaces()`. ### Feature Flags & Experiments Manage feature flags and experiments programmatically. Unlike dashboards/reports/cohorts, these are **project-scoped** and do not require a workspace ID. ``` import mixpanel_headless as mp ws = mp.Workspace() # Feature Flags flags = ws.list_feature_flags() flag = ws.create_feature_flag(mp.CreateFeatureFlagParams( name="Dark Mode", key="dark_mode" )) ws.update_feature_flag(flag.id, mp.UpdateFeatureFlagParams( name="Dark Mode", key="dark_mode", status=mp.FeatureFlagStatus.ENABLED, ruleset=flag.ruleset, )) # Experiments (full lifecycle) exp = ws.create_experiment(mp.CreateExperimentParams(name="Checkout Flow Test")) launched = ws.launch_experiment(exp.id) concluded = ws.conclude_experiment(exp.id) decided = ws.decide_experiment(exp.id, mp.ExperimentDecideParams(success=True)) ``` Feature flag `update` uses **PUT semantics** (all required fields must be provided). Experiment `update` uses **PATCH semantics** (only changed fields needed). See the [Entity Management guide](https://mixpanel.github.io/mixpanel-headless/guide/entity-management/index.md) for complete coverage. ### Data Governance Manage Lexicon definitions, drop filters, custom properties, custom events, and lookup tables programmatically. All operations are **workspace-scoped**. ``` import mixpanel_headless as mp ws = mp.Workspace() # Lexicon β€” Event and property definitions defs = ws.get_event_definitions(names=["Signup", "Login"]) ws.update_event_definition("Signup", mp.UpdateEventDefinitionParams(verified=True)) tags = ws.list_lexicon_tags() # Drop filters filters = ws.list_drop_filters() ws.create_drop_filter(mp.CreateDropFilterParams( event_name="Debug Event", filters={"property": "env", "value": "test"}, )) # Custom properties props = ws.list_custom_properties() prop = ws.get_custom_property("abc123") # Lookup tables tables = ws.list_lookup_tables() table = ws.upload_lookup_table(mp.UploadLookupTableParams( name="Countries", file_path="/path/to/countries.csv", )) # Custom events events = ws.list_custom_events() ``` See the [Data Governance guide](https://mixpanel.github.io/mixpanel-headless/guide/data-governance/index.md) for complete coverage. ### Business Context Read and write the markdown documentation that grounds AI assistants. Two scopes (`level="organization"` shared across the org, `level="project"` per-project), 50,000-character cap enforced client-side before any HTTP call. ``` import mixpanel_headless as mp ws = mp.Workspace() # Read project_ctx = ws.get_business_context(level="project") org_ctx = ws.get_business_context(level="organization") # auto-resolves org_id # Read both in one round-trip chain = ws.get_business_context_chain() # Write (full-replace; pass "" to clear) ws.set_business_context("# About Acme\n…", level="project") ws.clear_business_context(level="organization") ``` Project-scope writes require `edit_project_info` permission; org-scope writes require `edit_project_info` at the org level. See the [Business Context guide](https://mixpanel.github.io/mixpanel-headless/guide/business-context/index.md) for full coverage of input modes, error handling, and cross-project audit patterns. `workspaces()` vs `list_workspaces()` Both methods are exposed. `workspaces()` (recommended) returns `list[WorkspaceRef]` from the cached `/me` response β€” fast, typed, and consistent with `events()` / `properties()` / `funnels()` / `cohorts()`. `list_workspaces()` is a lower-level escape hatch that calls `GET /api/app/projects/{pid}/workspaces/public` directly and returns `list[PublicWorkspace]`. ## In-Session Switching `Workspace.use()` swaps the active account, project, workspace, or target without rebuilding the underlying `httpx.Client` or per-account `/me` cache. It returns `self` for fluent chaining, so cross-project iteration is O(1) per swap. ``` import mixpanel_headless as mp ws = mp.Workspace() ws.use(account="team") # implicitly clears workspace ws.use(project="3018488") ws.use(workspace=3448414) ws.use(target="ecom") # apply all three at once # Persist the new state ws.use(project="3018488", persist=True) # writes [active] # Read the resolved state print(ws.account.name, ws.project.id, ws.workspace.id if ws.workspace else None) print(ws.session) # full Session snapshot ``` See [Auth β†’ Workspace.use()](https://mixpanel.github.io/mixpanel-headless/api/auth/#workspaceuse-in-session-switching) for the resolution semantics and parallel-snapshot patterns. ## Class Reference ## mixpanel_headless.Workspace ``` Workspace( *, account: str | None = None, project: str | None = None, workspace: int | None = None, target: str | None = None, session: Session | None = None, _api_client: MixpanelAPIClient | None = None, ) ``` Unified entry point for Mixpanel data operations. The Workspace class is a facade that orchestrates: - DiscoveryService for schema exploration - LiveQueryService for real-time analytics - App API client for CRUD and data governance operations Examples: Basic usage with credentials from config: ``` ws = Workspace() events = ws.events() # discover schema result = ws.segmentation(event="login", from_date="2024-01-01", to_date="2024-01-31") ws.close() ``` Stream events for external processing: ``` ws = Workspace() for event in ws.stream_events(from_date="2024-01-01", to_date="2024-01-31"): process(event) ws.close() ``` Create a new Workspace bound to a resolved :class:`Session`. Resolution priority follows FR-017: env vars > kwargs > target > bridge > `[active]` > `Account.default_project`. Pass `session=` to bypass the resolver and use a pre-built :class:`Session` directly. | PARAMETER | DESCRIPTION | | ------------- | ------------------------------------------------------------------------------------------------------------ | | `account` | Named account from ~/.mp/config.toml. **TYPE:** \`str | | `project` | Project ID override (digit string). **TYPE:** \`str | | `workspace` | Workspace ID override (positive int). **TYPE:** \`int | | `target` | Apply all three axes from [targets.NAME]. Mutually exclusive with account/project/workspace. **TYPE:** \`str | | `session` | Pre-built :class:Session (full resolver bypass). **TYPE:** \`Session | | `_api_client` | Injected :class:MixpanelAPIClient for testing. **TYPE:** \`MixpanelAPIClient | | RAISES | DESCRIPTION | | ------------- | ------------------------------------------- | | `ValueError` | target= combined with any axis kwarg. | | `ConfigError` | Account or project axis cannot be resolved. | | `OAuthError` | Auth header construction fails. | Source code in `src/mixpanel_headless/workspace.py` ``` def __init__( self, *, account: str | None = None, project: str | None = None, workspace: int | None = None, target: str | None = None, session: _Session | None = None, _api_client: MixpanelAPIClient | None = None, ) -> None: """Create a new Workspace bound to a resolved :class:`Session`. Resolution priority follows FR-017: env vars > kwargs > target > bridge > ``[active]`` > ``Account.default_project``. Pass ``session=`` to bypass the resolver and use a pre-built :class:`Session` directly. Args: account: Named account from ``~/.mp/config.toml``. project: Project ID override (digit string). workspace: Workspace ID override (positive int). target: Apply all three axes from ``[targets.NAME]``. Mutually exclusive with ``account``/``project``/``workspace``. session: Pre-built :class:`Session` (full resolver bypass). _api_client: Injected :class:`MixpanelAPIClient` for testing. Raises: ValueError: ``target=`` combined with any axis kwarg. ConfigError: Account or project axis cannot be resolved. OAuthError: Auth header construction fails. """ if target is not None and ( account is not None or project is not None or workspace is not None ): raise ValueError( "`target=` is mutually exclusive with " "`account=`/`project=`/`workspace=`." ) self._discovery: DiscoveryService | None = None self._live_query: LiveQueryService | None = None self._me_service: MeService | None = None if session is not None: sess = session else: from mixpanel_headless._internal.auth.bridge import load_bridge br = load_bridge() # If the bridge has oauth_browser tokens embedded, materialize them # to the per-account on-disk path so the OnDiskTokenResolver can # serve them downstream. This is the Cowork credential-courier # contract: the bridge is the source of truth at startup. if ( br is not None and br.tokens is not None and br.account.type == "oauth_browser" ): from mixpanel_headless._internal.auth.storage import ( ensure_account_dir, ) from mixpanel_headless._internal.auth.token import token_payload_bytes from mixpanel_headless._internal.io_utils import atomic_write_bytes tokens_path = ensure_account_dir(br.account.name) / "tokens.json" # Always overwrite β€” the bridge is the authoritative # source of truth at startup, so a refreshed payload from # the host must replace any stale on-disk cache here. # ``OAuthTokens.expires_at`` is always set (required, tz-aware # per Fix 25) β€” no fall-through to None which would trip the # OnDiskTokenResolver expiry check. Empty scopes from the # bridge get a ``"read"`` default so the cached file matches # what `mp account login` would have written. tokens_to_persist = br.tokens if not tokens_to_persist.scope: tokens_to_persist = tokens_to_persist.model_copy( update={"scope": "read"} ) # atomic_write_bytes creates the file with 0o600 via O_EXCL # before any data is written, eliminating the umask-derived # permission window left open by write_text + chmod. atomic_write_bytes(tokens_path, token_payload_bytes(tokens_to_persist)) sess = _resolve_session( account=account, project=project, workspace=workspace, target=target, config=ConfigManager(), bridge=br, ) self._session = sess self._account_name: str = sess.account.name self._initial_workspace_id = sess.workspace.id if sess.workspace else None if _api_client is not None: self._api_client: MixpanelAPIClient | None = _api_client else: self._api_client = MixpanelAPIClient(session=sess) ``` ### account ``` account: Account ``` Return the resolved :class:`Account` for the current session. ### project ``` project: Project ``` Return the resolved :class:`Project` for the current session. ### workspace ``` workspace: WorkspaceRef | None ``` Return the resolved :class:`WorkspaceRef` (or `None` for lazy). ### session ``` session: Session ``` Return the bound :class:`Session`. ### api ``` api: MixpanelAPIClient ``` Direct access to the Mixpanel API client. Use this escape hatch for Mixpanel API operations not covered by the Workspace class. The client handles authentication automatically. The client provides - `request(method, url, **kwargs)`: Make authenticated requests to any Mixpanel API endpoint. - `project_id`: The configured project ID for constructing URLs. - `region`: The configured region ('us', 'eu', or 'in'). | RETURNS | DESCRIPTION | | ------------------- | --------------------------------- | | `MixpanelAPIClient` | The underlying MixpanelAPIClient. | | RAISES | DESCRIPTION | | ------------- | --------------------------------- | | `ConfigError` | If API credentials not available. | Example Fetch event schema from the Lexicon Schemas API:: ``` import mixpanel_headless as mp from urllib.parse import quote ws = mp.Workspace() client = ws.api # Build the URL with proper encoding event_name = quote("Added To Cart", safe="") url = f"https://mixpanel.com/api/app/projects/{client.project_id}/schemas/event/{event_name}" # Make the authenticated request schema = client.request("GET", url) print(schema) ``` ### close ``` close() -> None ``` Close all resources (HTTP client). This method is idempotent and safe to call multiple times. Source code in `src/mixpanel_headless/workspace.py` ``` def close(self) -> None: """Close all resources (HTTP client). This method is idempotent and safe to call multiple times. """ # Close API client if we created one if self._api_client is not None: self._api_client.close() self._api_client = None ``` ### use ``` use( *, account: str | None = None, project: str | None = None, workspace: int | None = None, target: str | None = None, persist: bool = False, ) -> Workspace ``` Swap one or more session axes in place; return `self` for chaining. `target=` is mutually exclusive with `account=`/`project=`/ `workspace=`. The HTTP transport is preserved across all switches (per Research R5). When `account=` is supplied, the project axis re-resolves through the FR-017 chain ending at the new account's `default_project` (env `MP_PROJECT_ID` > explicit `project=` > new account's `default_project`). If no source provides a project, the call raises :class:`ConfigError` per FR-033 β€” the prior session's project is NEVER carried forward across an account swap because cross-account project access is not guaranteed. The workspace axis is cleared on account swap (workspaces are project-scoped; the prior workspace doesn't apply to the new project) β€” explicit `workspace=` or `MP_WORKSPACE_ID` env override is honored. | PARAMETER | DESCRIPTION | | ----------- | -------------------------------------------------------------------------------------- | | `account` | Replacement account name. **TYPE:** \`str | | `project` | Replacement project ID. **TYPE:** \`str | | `workspace` | Replacement workspace ID. **TYPE:** \`int | | `target` | Apply this target's three axes atomically. **TYPE:** \`str | | `persist` | When True, also write the new state to [active]. **TYPE:** `bool` **DEFAULT:** `False` | | RETURNS | DESCRIPTION | | ----------- | ------------------------- | | `Workspace` | self for fluent chaining. | | RAISES | DESCRIPTION | | ------------- | ------------------------------------------------------- | | `ValueError` | Mutually exclusive args, or referenced name missing. | | `OAuthError` | New auth header construction fails (atomic on success). | | `ConfigError` | account= swap cannot resolve a project axis. | Source code in `src/mixpanel_headless/workspace.py` ``` def use( self, *, account: str | None = None, project: str | None = None, workspace: int | None = None, target: str | None = None, persist: bool = False, ) -> Workspace: """Swap one or more session axes in place; return ``self`` for chaining. ``target=`` is mutually exclusive with ``account=``/``project=``/ ``workspace=``. The HTTP transport is preserved across all switches (per Research R5). When ``account=`` is supplied, the project axis re-resolves through the FR-017 chain ending at the new account's ``default_project`` (env ``MP_PROJECT_ID`` > explicit ``project=`` > new account's ``default_project``). If no source provides a project, the call raises :class:`ConfigError` per FR-033 β€” the prior session's project is NEVER carried forward across an account swap because cross-account project access is not guaranteed. The workspace axis is cleared on account swap (workspaces are project-scoped; the prior workspace doesn't apply to the new project) β€” explicit ``workspace=`` or ``MP_WORKSPACE_ID`` env override is honored. Args: account: Replacement account name. project: Replacement project ID. workspace: Replacement workspace ID. target: Apply this target's three axes atomically. persist: When ``True``, also write the new state to ``[active]``. Returns: ``self`` for fluent chaining. Raises: ValueError: Mutually exclusive args, or referenced name missing. OAuthError: New auth header construction fails (atomic on success). ConfigError: ``account=`` swap cannot resolve a project axis. """ if target is not None and ( account is not None or project is not None or workspace is not None ): raise ValueError( "`target=` is mutually exclusive with `account=`/`project=`/`workspace=`." ) cm = ConfigManager() client = self._require_api_client() new_account_obj: _AccountUnion | None = None new_project_obj: _Project | None = None new_workspace_obj: _WorkspaceRef | None = None if target is not None: # Route through the same resolver as Workspace() construction so # env > param > target > bridge > config ordering applies (FR-017). # Without this, mid-process env-var overrides would be honored at # construction but silently ignored on `ws.use(target=...)`. sess = _resolve_session( target=target, config=cm, bridge=_load_bridge(), ) new_account_obj = sess.account new_project_obj = sess.project new_workspace_obj = sess.workspace elif account is not None: # Explicit account swap: the user told us which account to use, # so the env-vars-override-param rule (FR-017) on the account # axis doesn't apply here β€” load the requested account directly. # Project re-resolves through the FR-017 chain ending at the # NEW account's default_project (env > explicit > new account's # default); raises ConfigError if nothing resolves (per FR-033, # cross-account project access is not guaranteed). # Workspace is cleared (workspaces are project-scoped; the # prior workspace is meaningless under the new account/project) # β€” explicit `workspace=` overrides the clear, and env override # via MP_WORKSPACE_ID still applies for parity with FR-017. new_account_obj = cm.get_account(account) br = _load_bridge() project_id = _resolve_project_axis( explicit=project, target_project=None, bridge=br, account=new_account_obj, ) if project_id is None: raise ConfigError(_format_no_project_error(new_account_obj)) new_project_obj = _Project(id=project_id) # Account-swap intentionally clears workspace per FR-033 (workspaces # are project-scoped; the prior workspace doesn't apply to the new # project). Only an explicit ``workspace=`` kwarg or a validated # ``MP_WORKSPACE_ID`` env var can populate it. We bypass # ``resolve_workspace_axis`` because that consults ``[active].workspace`` # β€” which is exactly the fallback we need to skip here. if workspace is not None: new_workspace_obj = _WorkspaceRef(id=workspace) else: env_ws = _env_workspace_id() new_workspace_obj = ( _WorkspaceRef(id=env_ws) if env_ws is not None else None ) else: new_project_obj = _Project(id=project) if project is not None else None new_workspace_obj = ( _WorkspaceRef(id=workspace) if workspace is not None else None ) client.use( account=new_account_obj, project=new_project_obj, workspace=new_workspace_obj, ) self._session = client.session # Clear lazy services so subsequent reads of `project` / `account` / # `workspaces()` / `_me_svc` observe the new session rather than the # prior one. self._account_name = self._session.account.name self._initial_workspace_id = ( self._session.workspace.id if self._session.workspace else None ) self._discovery = None self._live_query = None self._me_service = None if persist: self._persist_active() return self ``` ### me ``` me(*, force_refresh: bool = False) -> Any ``` Get /me response for current credentials (cached 24h). Returns the authenticated user's profile including all accessible organizations, projects, and workspaces. | PARAMETER | DESCRIPTION | | --------------- | ----------------------------------------------------------------------------- | | `force_refresh` | If True, bypass cache and call the API. **TYPE:** `bool` **DEFAULT:** `False` | | RETURNS | DESCRIPTION | | ------- | ------------------------------------------------------- | | `Any` | MeResponse with user profile, projects, and workspaces. | | RAISES | DESCRIPTION | | ------------- | -------------------------------------------- | | `ConfigError` | If credentials lack /me access (401 or 403). | | `QueryError` | If the API returns a non-403 error. | Example ``` ws = Workspace() me = ws.me() print(me.user_email) for pid, proj in me.projects.items(): print(f" {pid}: {proj.name}") ``` Source code in `src/mixpanel_headless/workspace.py` ```` def me(self, *, force_refresh: bool = False) -> Any: """Get /me response for current credentials (cached 24h). Returns the authenticated user's profile including all accessible organizations, projects, and workspaces. Args: force_refresh: If True, bypass cache and call the API. Returns: MeResponse with user profile, projects, and workspaces. Raises: ConfigError: If credentials lack /me access (401 or 403). QueryError: If the API returns a non-403 error. Example: ```python ws = Workspace() me = ws.me() print(me.user_email) for pid, proj in me.projects.items(): print(f" {pid}: {proj.name}") ``` """ return self._me_svc.fetch(force_refresh=force_refresh) ```` ### projects ``` projects(*, refresh: bool = False) -> list[_Project] ``` List all accessible projects via the /me API (FR-035). Returns projects from the cached /me response, sorted by name. Each entry is a v3 :class:`Project` (id + name + organization_id + timezone), built from the underlying `MeProjectInfo` payload β€” callers iterate `for project in ws.projects(): ws.use(project=project.id)` per the documented cross-project iteration pattern. Replaces the deprecated `discover_projects()` (which returned `list[tuple[str, MeProjectInfo]]`) β€” for the raw `/me` shape with extra fields (`has_workspaces`, `domain`, `type`, ...), call `self._me_svc.list_projects()` directly from internal code. | PARAMETER | DESCRIPTION | | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | | `refresh` | When True, bypass the on-disk and in-memory /me caches and refetch from the API. Default False uses the 24h cache. **TYPE:** `bool` **DEFAULT:** `False` | | RETURNS | DESCRIPTION | | --------------- | ---------------------------------------------- | | `list[Project]` | List of :class:Project records sorted by name. | | RAISES | DESCRIPTION | | ------------- | ------------------------------- | | `ConfigError` | If credentials lack /me access. | Example ``` ws = Workspace() for project in ws.projects(): ws.use(project=project.id) print(project.id, project.name, len(ws.events())) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def projects(self, *, refresh: bool = False) -> list[_Project]: """List all accessible projects via the /me API (FR-035). Returns projects from the cached /me response, sorted by name. Each entry is a v3 :class:`Project` (id + name + organization_id + timezone), built from the underlying ``MeProjectInfo`` payload β€” callers iterate ``for project in ws.projects(): ws.use(project=project.id)`` per the documented cross-project iteration pattern. Replaces the deprecated ``discover_projects()`` (which returned ``list[tuple[str, MeProjectInfo]]``) β€” for the raw ``/me`` shape with extra fields (``has_workspaces``, ``domain``, ``type``, ...), call ``self._me_svc.list_projects()`` directly from internal code. Args: refresh: When True, bypass the on-disk and in-memory ``/me`` caches and refetch from the API. Default False uses the 24h cache. Returns: List of :class:`Project` records sorted by name. Raises: ConfigError: If credentials lack /me access. Example: ```python ws = Workspace() for project in ws.projects(): ws.use(project=project.id) print(project.id, project.name, len(ws.events())) ``` """ if refresh: self._me_svc.fetch(force_refresh=True) return [ _Project( id=pid, name=info.name, organization_id=info.organization_id, timezone=info.timezone, ) for pid, info in self._me_svc.list_projects() ] ```` ### workspaces ``` workspaces( *, project_id: str | None = None, refresh: bool = False ) -> list[_WorkspaceRef] ``` List workspaces for a project via the /me API (FR-036). Returns workspaces from the cached /me response, sorted by name. Defaults to the current project if `project_id` is not provided. Replaces the deprecated `discover_workspaces()` (which returned `list[MeWorkspaceInfo]`) β€” for the raw `/me` shape with extra fields (`is_global`, `is_restricted`, `description`, ...), call `self._me_svc.list_workspaces(project_id=)` directly from internal code. | PARAMETER | DESCRIPTION | | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `project_id` | Project ID to list workspaces for. Defaults to the current project. **TYPE:** \`str | | `refresh` | When True, bypass the on-disk and in-memory /me caches and refetch from the API. Default False uses the 24h cache. Mirrors :meth:projects(refresh=) (FR-047). **TYPE:** `bool` **DEFAULT:** `False` | | RETURNS | DESCRIPTION | | -------------------- | --------------------------------------------------- | | `list[WorkspaceRef]` | List of :class:WorkspaceRef records sorted by name. | | RAISES | DESCRIPTION | | ------------- | ------------------------------- | | `ConfigError` | If credentials lack /me access. | Example ``` ws = Workspace() for workspace in ws.workspaces(): print(workspace.id, workspace.name, workspace.is_default) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def workspaces( self, *, project_id: str | None = None, refresh: bool = False, ) -> list[_WorkspaceRef]: """List workspaces for a project via the /me API (FR-036). Returns workspaces from the cached /me response, sorted by name. Defaults to the current project if ``project_id`` is not provided. Replaces the deprecated ``discover_workspaces()`` (which returned ``list[MeWorkspaceInfo]``) β€” for the raw ``/me`` shape with extra fields (``is_global``, ``is_restricted``, ``description``, ...), call ``self._me_svc.list_workspaces(project_id=)`` directly from internal code. Args: project_id: Project ID to list workspaces for. Defaults to the current project. refresh: When True, bypass the on-disk and in-memory ``/me`` caches and refetch from the API. Default False uses the 24h cache. Mirrors :meth:`projects(refresh=)` (FR-047). Returns: List of :class:`WorkspaceRef` records sorted by name. Raises: ConfigError: If credentials lack /me access. Example: ```python ws = Workspace() for workspace in ws.workspaces(): print(workspace.id, workspace.name, workspace.is_default) ``` """ if refresh: self._me_svc.fetch(force_refresh=True) pid = project_id if pid is None: pid = self._session.project.id return [ _WorkspaceRef(id=info.id, name=info.name, is_default=info.is_default) for info in self._me_svc.list_workspaces(project_id=pid) ] ```` ### list_workspaces ``` list_workspaces() -> list[PublicWorkspace] ``` List all public workspaces for the current project. Delegates to the API client's `list_workspaces()` method, which calls `GET /api/app/projects/{pid}/workspaces/public`. | RETURNS | DESCRIPTION | | ----------------------- | ----------------------------------------------- | | `list[PublicWorkspace]` | List of PublicWorkspace models for the project. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | API error (400, 404). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() workspaces = ws.list_workspaces() for w in workspaces: print(f"{w.name} (id={w.id}, default={w.is_default})") ``` Source code in `src/mixpanel_headless/workspace.py` ```` def list_workspaces(self) -> list[PublicWorkspace]: """List all public workspaces for the current project. Delegates to the API client's ``list_workspaces()`` method, which calls ``GET /api/app/projects/{pid}/workspaces/public``. Returns: List of ``PublicWorkspace`` models for the project. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: API error (400, 404). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() workspaces = ws.list_workspaces() for w in workspaces: print(f"{w.name} (id={w.id}, default={w.is_default})") ``` """ client = self._require_api_client() return client.list_workspaces() ```` ### resolve_workspace_id ``` resolve_workspace_id() -> int ``` Resolve the workspace ID for scoped requests. Resolution order: 1. Workspace ID already pinned on the resolved session (for example via `Workspace(workspace=N)`, `Workspace.use(workspace=N)`, `MP_WORKSPACE_ID`, saved targets, bridge pins, or persisted `[active].workspace` state) 1. Cached auto-discovered workspace ID 1. Auto-discover by listing workspaces and finding the default | RETURNS | DESCRIPTION | | ------- | -------------------------- | | `int` | The resolved workspace ID. | | RAISES | DESCRIPTION | | --------------------- | ------------------------------------------- | | `ConfigError` | If credentials are not available. | | `WorkspaceScopeError` | If no workspaces are found for the project. | Example ``` ws = Workspace() ws_id = ws.resolve_workspace_id() print(f"Using workspace {ws_id}") ``` Source code in `src/mixpanel_headless/workspace.py` ```` def resolve_workspace_id(self) -> int: """Resolve the workspace ID for scoped requests. Resolution order: 1. Workspace ID already pinned on the resolved session (for example via ``Workspace(workspace=N)``, ``Workspace.use(workspace=N)``, ``MP_WORKSPACE_ID``, saved targets, bridge pins, or persisted ``[active].workspace`` state) 2. Cached auto-discovered workspace ID 3. Auto-discover by listing workspaces and finding the default Returns: The resolved workspace ID. Raises: ConfigError: If credentials are not available. WorkspaceScopeError: If no workspaces are found for the project. Example: ```python ws = Workspace() ws_id = ws.resolve_workspace_id() print(f"Using workspace {ws_id}") ``` """ client = self._require_api_client() return client.resolve_workspace_id() ```` ### events ``` events() -> list[str] ``` List all event names in the Mixpanel project. Results are cached for the lifetime of the Workspace. | RETURNS | DESCRIPTION | | ----------- | ------------------------------------------ | | `list[str]` | Alphabetically sorted list of event names. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If API credentials not available. | | `AuthenticationError` | If credentials are invalid. | Source code in `src/mixpanel_headless/workspace.py` ``` def events(self) -> list[str]: """List all event names in the Mixpanel project. Results are cached for the lifetime of the Workspace. Returns: Alphabetically sorted list of event names. Raises: ConfigError: If API credentials not available. AuthenticationError: If credentials are invalid. """ return self._discovery_service.list_events() ``` ### properties ``` properties(event: str) -> list[str] ``` List all property names for an event. Results are cached per event for the lifetime of the Workspace. | PARAMETER | DESCRIPTION | | --------- | ------------------------------------------------- | | `event` | Event name to get properties for. **TYPE:** `str` | | RETURNS | DESCRIPTION | | ----------- | --------------------------------------------- | | `list[str]` | Alphabetically sorted list of property names. | | RAISES | DESCRIPTION | | ------------- | --------------------------------- | | `ConfigError` | If API credentials not available. | Source code in `src/mixpanel_headless/workspace.py` ``` def properties(self, event: str) -> list[str]: """List all property names for an event. Results are cached per event for the lifetime of the Workspace. Args: event: Event name to get properties for. Returns: Alphabetically sorted list of property names. Raises: ConfigError: If API credentials not available. """ return self._discovery_service.list_properties(event) ``` ### property_values ``` property_values( property_name: str, *, event: str | None = None, limit: int = 100 ) -> list[str] ``` Get sample values for a property. Results are cached per (property, event, limit) for the lifetime of the Workspace. | PARAMETER | DESCRIPTION | | --------------- | ---------------------------------------------------------------------- | | `property_name` | Property to get values for. **TYPE:** `str` | | `event` | Optional event to filter by. **TYPE:** \`str | | `limit` | Maximum number of values to return. **TYPE:** `int` **DEFAULT:** `100` | | RETURNS | DESCRIPTION | | ----------- | ------------------------------------------ | | `list[str]` | List of sample property values as strings. | | RAISES | DESCRIPTION | | ------------- | --------------------------------- | | `ConfigError` | If API credentials not available. | Source code in `src/mixpanel_headless/workspace.py` ``` def property_values( self, property_name: str, *, event: str | None = None, limit: int = 100, ) -> list[str]: """Get sample values for a property. Results are cached per (property, event, limit) for the lifetime of the Workspace. Args: property_name: Property to get values for. event: Optional event to filter by. limit: Maximum number of values to return. Returns: List of sample property values as strings. Raises: ConfigError: If API credentials not available. """ return self._discovery_service.list_property_values( property_name, event=event, limit=limit ) ``` ### subproperties ``` subproperties( property_name: str, *, event: str | None = None, sample_size: int = 50 ) -> list[SubPropertyInfo] ``` List inferred subproperties of a list-of-object event property. Samples values via :meth:`property_values`, parses each as JSON, and returns one :class:`SubPropertyInfo` per discovered scalar subproperty. Designed for properties like `cart` whose values are objects with subkeys (`Brand`, `Category`, `Price`, `Item ID`). The returned `name` and `type` plug directly into :meth:`GroupBy.list_item` and :meth:`Filter.list_contains`. Scope: only **scalar** subproperty values (string / number / boolean / ISO datetime string) are reported. Subproperties whose values are themselves dicts or lists are silently skipped β€” they cannot be used by `GroupBy.list_item` or `Filter.list_contains` anyway. | PARAMETER | DESCRIPTION | | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | | `property_name` | Top-level list-of-object property name (e.g. "cart"). **TYPE:** `str` | | `event` | Optional event name to scope the sample. Strongly recommended; without it the API may return values from across all events. **TYPE:** \`str | | `sample_size` | Number of raw values to sample. Default: 50. **TYPE:** `int` **DEFAULT:** `50` | | RETURNS | DESCRIPTION | | ----------------------- | ----------------------------------------------------- | | `list[SubPropertyInfo]` | Alphabetically sorted list of :class:SubPropertyInfo. | | `list[SubPropertyInfo]` | Empty list if no parseable dict values were found. | | RAISES | DESCRIPTION | | --------------------- | ------------------------------------------------------- | | `ConfigError` | If API credentials cannot be resolved. | | `AuthenticationError` | If credentials are configured but rejected by Mixpanel. | | WARNS | DESCRIPTION | | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `UserWarning` | When a subproperty has values of mixed scalar types across rows (collapses to "string"); when a sub-key is observed with both scalar and nested-object shapes (reports the scalar form); or when a sub-key is observed but all sampled values were null (excluded from output). | Example ``` for sp in ws.subproperties("cart", event="Cart Viewed"): print(sp.name, sp.type, sp.sample_values) # Brand string ('nike', 'puma', 'h&m') # Category string ('hats', 'jeans') # Item ID number (35317, 35318) # Price number (51, 87, 102) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def subproperties( self, property_name: str, *, event: str | None = None, sample_size: int = 50, ) -> list[SubPropertyInfo]: """List inferred subproperties of a list-of-object event property. Samples values via :meth:`property_values`, parses each as JSON, and returns one :class:`SubPropertyInfo` per discovered scalar subproperty. Designed for properties like ``cart`` whose values are objects with subkeys (``Brand``, ``Category``, ``Price``, ``Item ID``). The returned ``name`` and ``type`` plug directly into :meth:`GroupBy.list_item` and :meth:`Filter.list_contains`. Scope: only **scalar** subproperty values (string / number / boolean / ISO datetime string) are reported. Subproperties whose values are themselves dicts or lists are silently skipped β€” they cannot be used by ``GroupBy.list_item`` or ``Filter.list_contains`` anyway. Args: property_name: Top-level list-of-object property name (e.g. ``"cart"``). event: Optional event name to scope the sample. Strongly recommended; without it the API may return values from across all events. sample_size: Number of raw values to sample. Default: 50. Returns: Alphabetically sorted list of :class:`SubPropertyInfo`. Empty list if no parseable dict values were found. Raises: ConfigError: If API credentials cannot be resolved. AuthenticationError: If credentials are configured but rejected by Mixpanel. Warns: UserWarning: When a subproperty has values of mixed scalar types across rows (collapses to ``"string"``); when a sub-key is observed with both scalar and nested-object shapes (reports the scalar form); or when a sub-key is observed but all sampled values were ``null`` (excluded from output). Example: ```python for sp in ws.subproperties("cart", event="Cart Viewed"): print(sp.name, sp.type, sp.sample_values) # Brand string ('nike', 'puma', 'h&m') # Category string ('hats', 'jeans') # Item ID number (35317, 35318) # Price number (51, 87, 102) ``` """ return self._discovery_service.list_subproperties( property_name, event=event, sample_size=sample_size ) ```` ### funnels ``` funnels() -> list[FunnelInfo] ``` List saved funnels in the Mixpanel project. Results are cached for the lifetime of the Workspace. | RETURNS | DESCRIPTION | | ------------------ | --------------------------------------------- | | `list[FunnelInfo]` | List of FunnelInfo objects (funnel_id, name). | | RAISES | DESCRIPTION | | ------------- | --------------------------------- | | `ConfigError` | If API credentials not available. | Source code in `src/mixpanel_headless/workspace.py` ``` def funnels(self) -> list[FunnelInfo]: """List saved funnels in the Mixpanel project. Results are cached for the lifetime of the Workspace. Returns: List of FunnelInfo objects (funnel_id, name). Raises: ConfigError: If API credentials not available. """ return self._discovery_service.list_funnels() ``` ### cohorts ``` cohorts() -> list[SavedCohort] ``` List saved cohorts in the Mixpanel project. Results are cached for the lifetime of the Workspace. | RETURNS | DESCRIPTION | | ------------------- | ---------------------------- | | `list[SavedCohort]` | List of SavedCohort objects. | | RAISES | DESCRIPTION | | ------------- | --------------------------------- | | `ConfigError` | If API credentials not available. | Source code in `src/mixpanel_headless/workspace.py` ``` def cohorts(self) -> list[SavedCohort]: """List saved cohorts in the Mixpanel project. Results are cached for the lifetime of the Workspace. Returns: List of SavedCohort objects. Raises: ConfigError: If API credentials not available. """ return self._discovery_service.list_cohorts() ``` ### list_bookmarks ``` list_bookmarks(bookmark_type: BookmarkType | None = None) -> list[BookmarkInfo] ``` List all saved reports (bookmarks) in the project. Retrieves metadata for all saved Insights, Funnel, Retention, and Flows reports in the project. | PARAMETER | DESCRIPTION | | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `bookmark_type` | Optional filter by report type. Valid values are 'insights', 'funnels', 'retention', 'flows', 'launch-analysis'. If None, returns all bookmark types. **TYPE:** \`BookmarkType | | RETURNS | DESCRIPTION | | -------------------- | -------------------------------------------------- | | `list[BookmarkInfo]` | List of BookmarkInfo objects with report metadata. | | `list[BookmarkInfo]` | Empty list if no bookmarks exist. | | RAISES | DESCRIPTION | | ------------- | -------------------------------------------- | | `ConfigError` | If API credentials not available. | | `QueryError` | Permission denied or invalid type parameter. | Source code in `src/mixpanel_headless/workspace.py` ``` def list_bookmarks( self, bookmark_type: BookmarkType | None = None, ) -> list[BookmarkInfo]: """List all saved reports (bookmarks) in the project. Retrieves metadata for all saved Insights, Funnel, Retention, and Flows reports in the project. Args: bookmark_type: Optional filter by report type. Valid values are 'insights', 'funnels', 'retention', 'flows', 'launch-analysis'. If None, returns all bookmark types. Returns: List of BookmarkInfo objects with report metadata. Empty list if no bookmarks exist. Raises: ConfigError: If API credentials not available. QueryError: Permission denied or invalid type parameter. """ return self._discovery_service.list_bookmarks(bookmark_type=bookmark_type) ``` ### top_events ``` top_events( *, type: Literal["general", "average", "unique"] = "general", limit: int | None = None, ) -> list[TopEvent] ``` Get today's most active events. This method is NOT cached (returns real-time data). | PARAMETER | DESCRIPTION | | --------- | ------------------------------------------------------------------------------------------------------------------------ | | `type` | Counting method (general, average, unique). **TYPE:** `Literal['general', 'average', 'unique']` **DEFAULT:** `'general'` | | `limit` | Maximum number of events to return. **TYPE:** \`int | | RETURNS | DESCRIPTION | | ---------------- | ----------------------------------------------- | | `list[TopEvent]` | List of TopEvent objects with event, count, and | | `list[TopEvent]` | percent_change fields. | | RAISES | DESCRIPTION | | ------------- | --------------------------------- | | `ConfigError` | If API credentials not available. | Example ``` top = ws.top_events(limit=10) for t in top: print(f"{t.event}: {t.count:,} ({t.percent_change:+.1%})") ``` Source code in `src/mixpanel_headless/workspace.py` ```` def top_events( self, *, type: Literal["general", "average", "unique"] = "general", limit: int | None = None, ) -> list[TopEvent]: """Get today's most active events. This method is NOT cached (returns real-time data). Args: type: Counting method (general, average, unique). limit: Maximum number of events to return. Returns: List of TopEvent objects with ``event``, ``count``, and ``percent_change`` fields. Raises: ConfigError: If API credentials not available. Example: ```python top = ws.top_events(limit=10) for t in top: print(f"{t.event}: {t.count:,} ({t.percent_change:+.1%})") ``` """ return self._discovery_service.list_top_events(type=type, limit=limit) ```` ### lexicon_schemas ``` lexicon_schemas( *, entity_type: EntityType | None = None ) -> list[LexiconSchema] ``` List Lexicon schemas in the project. Retrieves documented event and profile property schemas from the Mixpanel Lexicon (data dictionary). Results are cached for the lifetime of the Workspace. | PARAMETER | DESCRIPTION | | ------------- | ---------------------------------------------------------------------------------------------------- | | `entity_type` | Optional filter by type ("event" or "profile"). If None, returns all schemas. **TYPE:** \`EntityType | | RETURNS | DESCRIPTION | | --------------------- | ---------------------------------------------------- | | `list[LexiconSchema]` | Alphabetically sorted list of LexiconSchema objects. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If API credentials not available. | | `AuthenticationError` | If credentials are invalid. | Note The Lexicon API has a strict 5 requests/minute rate limit. Caching helps avoid hitting this limit; call clear_discovery_cache() only when fresh data is needed. Source code in `src/mixpanel_headless/workspace.py` ``` def lexicon_schemas( self, *, entity_type: EntityType | None = None, ) -> list[LexiconSchema]: """List Lexicon schemas in the project. Retrieves documented event and profile property schemas from the Mixpanel Lexicon (data dictionary). Results are cached for the lifetime of the Workspace. Args: entity_type: Optional filter by type ("event" or "profile"). If None, returns all schemas. Returns: Alphabetically sorted list of LexiconSchema objects. Raises: ConfigError: If API credentials not available. AuthenticationError: If credentials are invalid. Note: The Lexicon API has a strict 5 requests/minute rate limit. Caching helps avoid hitting this limit; call clear_discovery_cache() only when fresh data is needed. """ return self._discovery_service.list_schemas(entity_type=entity_type) ``` ### lexicon_schema ``` lexicon_schema(entity_type: EntityType, name: str) -> LexiconSchema ``` Get a single Lexicon schema by entity type and name. Retrieves a documented schema for a specific event or profile property from the Mixpanel Lexicon (data dictionary). Results are cached for the lifetime of the Workspace. | PARAMETER | DESCRIPTION | | ------------- | ---------------------------------------------------------- | | `entity_type` | Entity type ("event" or "profile"). **TYPE:** `EntityType` | | `name` | Entity name. **TYPE:** `str` | | RETURNS | DESCRIPTION | | --------------- | --------------------------------------- | | `LexiconSchema` | LexiconSchema for the specified entity. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If API credentials not available. | | `AuthenticationError` | If credentials are invalid. | | `QueryError` | If schema not found. | Note The Lexicon API has a strict 5 requests/minute rate limit. Caching helps avoid hitting this limit; call clear_discovery_cache() only when fresh data is needed. Source code in `src/mixpanel_headless/workspace.py` ``` def lexicon_schema( self, entity_type: EntityType, name: str, ) -> LexiconSchema: """Get a single Lexicon schema by entity type and name. Retrieves a documented schema for a specific event or profile property from the Mixpanel Lexicon (data dictionary). Results are cached for the lifetime of the Workspace. Args: entity_type: Entity type ("event" or "profile"). name: Entity name. Returns: LexiconSchema for the specified entity. Raises: ConfigError: If API credentials not available. AuthenticationError: If credentials are invalid. QueryError: If schema not found. Note: The Lexicon API has a strict 5 requests/minute rate limit. Caching helps avoid hitting this limit; call clear_discovery_cache() only when fresh data is needed. """ return self._discovery_service.get_schema(entity_type, name) ``` ### clear_discovery_cache ``` clear_discovery_cache() -> None ``` Clear cached discovery results. Subsequent discovery calls will fetch fresh data from the API. Source code in `src/mixpanel_headless/workspace.py` ``` def clear_discovery_cache(self) -> None: """Clear cached discovery results. Subsequent discovery calls will fetch fresh data from the API. """ if self._discovery is not None: self._discovery.clear_cache() ``` ### stream_events ``` stream_events( *, from_date: str, to_date: str, events: list[str] | None = None, where: str | None = None, limit: int | None = None, raw: bool = False, ) -> Iterator[dict[str, Any]] ``` Stream events directly from Mixpanel API without storing. Yields events one at a time as they are received from the API. No database files or tables are created. | PARAMETER | DESCRIPTION | | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `from_date` | Start date inclusive (YYYY-MM-DD format). **TYPE:** `str` | | `to_date` | End date inclusive (YYYY-MM-DD format). **TYPE:** `str` | | `events` | Optional list of event names to filter. If None, all events returned. **TYPE:** \`list[str] | | `where` | Optional Mixpanel filter expression (e.g., 'properties["country"]=="US"'). **TYPE:** \`str | | `limit` | Optional maximum number of events to return (max 100000). **TYPE:** \`int | | `raw` | If True, return events in raw Mixpanel API format. If False (default), return normalized format with datetime objects. **TYPE:** `bool` **DEFAULT:** `False` | | YIELDS | DESCRIPTION | | ---------------- | ----------------------------------------------------------------- | | `dict[str, Any]` | dict\[str, Any\]: Event dictionaries in normalized or raw format. | | RAISES | DESCRIPTION | | --------------------- | ------------------------------------------- | | `ConfigError` | If API credentials are not available. | | `AuthenticationError` | If credentials are invalid. | | `RateLimitError` | If rate limit exceeded after max retries. | | `QueryError` | If filter expression is invalid. | | `ValueError` | If limit is outside valid range (1-100000). | Example ``` ws = Workspace() for event in ws.stream_events(from_date="2024-01-01", to_date="2024-01-31"): process(event) ws.close() ``` With raw format: ``` for event in ws.stream_events( from_date="2024-01-01", to_date="2024-01-31", raw=True ): legacy_system.ingest(event) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def stream_events( self, *, from_date: str, to_date: str, events: list[str] | None = None, where: str | None = None, limit: int | None = None, raw: bool = False, ) -> Iterator[dict[str, Any]]: """Stream events directly from Mixpanel API without storing. Yields events one at a time as they are received from the API. No database files or tables are created. Args: from_date: Start date inclusive (YYYY-MM-DD format). to_date: End date inclusive (YYYY-MM-DD format). events: Optional list of event names to filter. If None, all events returned. where: Optional Mixpanel filter expression (e.g., 'properties["country"]=="US"'). limit: Optional maximum number of events to return (max 100000). raw: If True, return events in raw Mixpanel API format. If False (default), return normalized format with datetime objects. Yields: dict[str, Any]: Event dictionaries in normalized or raw format. Raises: ConfigError: If API credentials are not available. AuthenticationError: If credentials are invalid. RateLimitError: If rate limit exceeded after max retries. QueryError: If filter expression is invalid. ValueError: If limit is outside valid range (1-100000). Example: ```python ws = Workspace() for event in ws.stream_events(from_date="2024-01-01", to_date="2024-01-31"): process(event) ws.close() ``` With raw format: ```python for event in ws.stream_events( from_date="2024-01-01", to_date="2024-01-31", raw=True ): legacy_system.ingest(event) ``` """ # Validate limit early to avoid wasted API calls _validate_limit(limit) api_client = self._require_api_client() event_iterator = api_client.export_events( from_date=from_date, to_date=to_date, events=events, where=where, limit=limit, ) if raw: yield from event_iterator else: for event in event_iterator: yield transform_event(event) ```` ### stream_profiles ``` stream_profiles( *, where: str | None = None, cohort_id: str | None = None, output_properties: list[str] | None = None, raw: bool = False, distinct_id: str | None = None, distinct_ids: list[str] | None = None, group_id: str | None = None, behaviors: list[dict[str, Any]] | None = None, as_of_timestamp: int | None = None, include_all_users: bool = False, ) -> Iterator[dict[str, Any]] ``` Stream user profiles directly from Mixpanel API without storing. Yields profiles one at a time as they are received from the API. No database files or tables are created. | PARAMETER | DESCRIPTION | | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `where` | Optional Mixpanel filter expression for profile properties. **TYPE:** \`str | | `cohort_id` | Optional cohort ID to filter by. Only profiles that are members of this cohort will be returned. **TYPE:** \`str | | `output_properties` | Optional list of property names to include in the response. If None, all properties are returned. **TYPE:** \`list[str] | | `raw` | If True, return profiles in raw Mixpanel API format. If False (default), return normalized format. **TYPE:** `bool` **DEFAULT:** `False` | | `distinct_id` | Optional single user ID to fetch. Mutually exclusive with distinct_ids. **TYPE:** \`str | | `distinct_ids` | Optional list of user IDs to fetch. Mutually exclusive with distinct_id. Duplicates are automatically removed. **TYPE:** \`list[str] | | `group_id` | Optional group type identifier (e.g., "companies") to fetch group profiles instead of user profiles. **TYPE:** \`str | | `behaviors` | Optional list of behavioral filters. Each dict should have 'window' (e.g., "30d"), 'name' (identifier), and 'event_selectors' (list of {"event": "Name"}). Use with where parameter to filter, e.g., where='(behaviors["name"] > 0)'. Mutually exclusive with cohort_id. **TYPE:** \`list\[dict[str, Any]\] | | `as_of_timestamp` | Optional Unix timestamp to query profile state at a specific point in time. Must be in the past. **TYPE:** \`int | | `include_all_users` | If True, include all users and mark cohort membership. Only valid when cohort_id is provided. **TYPE:** `bool` **DEFAULT:** `False` | | YIELDS | DESCRIPTION | | ---------------- | ------------------------------------------------------------------- | | `dict[str, Any]` | dict\[str, Any\]: Profile dictionaries in normalized or raw format. | | RAISES | DESCRIPTION | | --------------------- | ---------------------------------------------- | | `ConfigError` | If API credentials are not available. | | `AuthenticationError` | If credentials are invalid. | | `RateLimitError` | If rate limit exceeded after max retries. | | `ValueError` | If mutually exclusive parameters are provided. | Example ``` ws = Workspace() for profile in ws.stream_profiles(): sync_to_crm(profile) ws.close() ``` Filter to premium users: ``` for profile in ws.stream_profiles(where='properties["plan"]=="premium"'): send_survey(profile) ``` Filter by cohort and select specific properties: ``` for profile in ws.stream_profiles( cohort_id="12345", output_properties=["$email", "$name"] ): send_email(profile) ``` Fetch specific users by ID: ``` for profile in ws.stream_profiles(distinct_ids=["user_1", "user_2"]): print(profile) ``` Fetch group profiles: ``` for company in ws.stream_profiles(group_id="companies"): print(company) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def stream_profiles( self, *, where: str | None = None, cohort_id: str | None = None, output_properties: list[str] | None = None, raw: bool = False, distinct_id: str | None = None, distinct_ids: list[str] | None = None, group_id: str | None = None, behaviors: list[dict[str, Any]] | None = None, as_of_timestamp: int | None = None, include_all_users: bool = False, ) -> Iterator[dict[str, Any]]: """Stream user profiles directly from Mixpanel API without storing. Yields profiles one at a time as they are received from the API. No database files or tables are created. Args: where: Optional Mixpanel filter expression for profile properties. cohort_id: Optional cohort ID to filter by. Only profiles that are members of this cohort will be returned. output_properties: Optional list of property names to include in the response. If None, all properties are returned. raw: If True, return profiles in raw Mixpanel API format. If False (default), return normalized format. distinct_id: Optional single user ID to fetch. Mutually exclusive with distinct_ids. distinct_ids: Optional list of user IDs to fetch. Mutually exclusive with distinct_id. Duplicates are automatically removed. group_id: Optional group type identifier (e.g., "companies") to fetch group profiles instead of user profiles. behaviors: Optional list of behavioral filters. Each dict should have 'window' (e.g., "30d"), 'name' (identifier), and 'event_selectors' (list of {"event": "Name"}). Use with `where` parameter to filter, e.g., where='(behaviors["name"] > 0)'. Mutually exclusive with cohort_id. as_of_timestamp: Optional Unix timestamp to query profile state at a specific point in time. Must be in the past. include_all_users: If True, include all users and mark cohort membership. Only valid when cohort_id is provided. Yields: dict[str, Any]: Profile dictionaries in normalized or raw format. Raises: ConfigError: If API credentials are not available. AuthenticationError: If credentials are invalid. RateLimitError: If rate limit exceeded after max retries. ValueError: If mutually exclusive parameters are provided. Example: ```python ws = Workspace() for profile in ws.stream_profiles(): sync_to_crm(profile) ws.close() ``` Filter to premium users: ```python for profile in ws.stream_profiles(where='properties["plan"]=="premium"'): send_survey(profile) ``` Filter by cohort and select specific properties: ```python for profile in ws.stream_profiles( cohort_id="12345", output_properties=["$email", "$name"] ): send_email(profile) ``` Fetch specific users by ID: ```python for profile in ws.stream_profiles(distinct_ids=["user_1", "user_2"]): print(profile) ``` Fetch group profiles: ```python for company in ws.stream_profiles(group_id="companies"): print(company) ``` """ api_client = self._require_api_client() profile_iterator = api_client.export_profiles( where=where, cohort_id=cohort_id, output_properties=output_properties, distinct_id=distinct_id, distinct_ids=distinct_ids, group_id=group_id, behaviors=behaviors, as_of_timestamp=as_of_timestamp, include_all_users=include_all_users, ) if raw: yield from profile_iterator else: for profile in profile_iterator: yield transform_profile(profile) ```` ### get_business_context ``` get_business_context( *, level: Literal["organization", "project"] = "project", organization_id: int | None = None, ) -> BusinessContext ``` Read business context content at the given scope. Calls `GET /api/app/projects/{pid}/business-context` (when `level="project"`) or `GET /api/app/organizations/{org_id}/business-context` (when `level="organization"`). Returns a populated `BusinessContext` with `content=""` when no context is set. | PARAMETER | DESCRIPTION | | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `level` | "project" (default) reads the project-level context for the active session's project. "organization" reads the org-level context shared across all projects in the organization. **TYPE:** `Literal['organization', 'project']` **DEFAULT:** `'project'` | | `organization_id` | Optional explicit org ID, only honored when level="organization". When omitted, the org ID is auto-resolved from the cached /me response (which may trigger a /me API call when the cache is cold). **TYPE:** \`int | | RETURNS | DESCRIPTION | | ----------------- | --------------------------------------------------- | | `BusinessContext` | BusinessContext whose content reflects the server's | | `BusinessContext` | current state. organization_id is populated for | | `BusinessContext` | org-level returns; project_id for project-level. | | RAISES | DESCRIPTION | | ----------------------- | --------------------------------------------------------------- | | `ValueError` | level is not "organization" or "project". | | `ConfigError` | Credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | API error (400, 403, 404). | | `ServerError` | Server-side errors (5xx). | | `WorkspaceScopeError` | level="organization" and the org ID could not be auto-resolved. | | `MixpanelHeadlessError` | API response is missing the content field. | Example ``` ws = Workspace() project_ctx = ws.get_business_context(level="project") org_ctx = ws.get_business_context( level="organization", organization_id=100, ) print(project_ctx.content) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def get_business_context( self, *, level: Literal["organization", "project"] = "project", organization_id: int | None = None, ) -> BusinessContext: """Read business context content at the given scope. Calls ``GET /api/app/projects/{pid}/business-context`` (when ``level="project"``) or ``GET /api/app/organizations/{org_id}/business-context`` (when ``level="organization"``). Returns a populated ``BusinessContext`` with ``content=""`` when no context is set. Args: level: ``"project"`` (default) reads the project-level context for the active session's project. ``"organization"`` reads the org-level context shared across all projects in the organization. organization_id: Optional explicit org ID, only honored when ``level="organization"``. When omitted, the org ID is auto-resolved from the cached ``/me`` response (which may trigger a ``/me`` API call when the cache is cold). Returns: ``BusinessContext`` whose ``content`` reflects the server's current state. ``organization_id`` is populated for org-level returns; ``project_id`` for project-level. Raises: ValueError: ``level`` is not ``"organization"`` or ``"project"``. ConfigError: Credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: API error (400, 403, 404). ServerError: Server-side errors (5xx). WorkspaceScopeError: ``level="organization"`` and the org ID could not be auto-resolved. MixpanelHeadlessError: API response is missing the ``content`` field. Example: ```python ws = Workspace() project_ctx = ws.get_business_context(level="project") org_ctx = ws.get_business_context( level="organization", organization_id=100, ) print(project_ctx.content) ``` """ self._validate_level(level) client = self._require_api_client() if level == "organization": org_id = self._resolve_organization_id(organization_id) raw = client.get_business_context(organization_id=org_id) return BusinessContext( level="organization", content=self._require_str_field( raw, "content", method="get_business_context", ), organization_id=org_id, ) raw = client.get_business_context() return BusinessContext( level="project", content=self._require_str_field( raw, "content", method="get_business_context", ), project_id=self._session.project.id, ) ```` ### set_business_context ``` set_business_context( content: str, *, level: Literal["organization", "project"] = "project", organization_id: int | None = None, ) -> BusinessContext ``` Replace business context content at the given scope. Validates `len(content) <= BUSINESS_CONTEXT_MAX_CHARS` (50,000) client-side BEFORE the HTTP call so callers fail fast and avoid a wasted round-trip to the server (which enforces the same limit and returns 400 above it). Then calls `PUT /api/app/projects/{pid}/business-context` (project) or `PUT /api/app/organizations/{org_id}/business-context` (org). The PUT is full-replace β€” pass an empty string to clear (or use `clear_business_context` for clarity). | PARAMETER | DESCRIPTION | | ----------------- | ---------------------------------------------------------------------------------------------------------------------- | | `content` | New markdown content. Empty string clears the context at this scope. **TYPE:** `str` | | `level` | "project" (default) or "organization". **TYPE:** `Literal['organization', 'project']` **DEFAULT:** `'project'` | | `organization_id` | Optional explicit org ID, only honored when level="organization". Auto-resolved from /me when omitted. **TYPE:** \`int | | RETURNS | DESCRIPTION | | ----------------- | --------------------------------------------------- | | `BusinessContext` | BusinessContext echoing the server's saved content. | | RAISES | DESCRIPTION | | -------------------------------- | ------------------------------------------------------------------------- | | `ValueError` | level is not "organization" or "project". | | `BusinessContextValidationError` | len(content) > 50_000 (client-side check, no HTTP call made). | | `ConfigError` | Credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Caller lacks edit_project_info permission (403) or other API error (400). | | `ServerError` | Server-side errors (5xx). | | `WorkspaceScopeError` | level="organization" and the org ID could not be auto-resolved. | | `MixpanelHeadlessError` | API response is missing the content field. | Example ``` ws = Workspace() ws.set_business_context("# Acme Corp\n...", level="project") ws.set_business_context( "# Org-wide context", level="organization", ) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def set_business_context( self, content: str, *, level: Literal["organization", "project"] = "project", organization_id: int | None = None, ) -> BusinessContext: """Replace business context content at the given scope. Validates ``len(content) <= BUSINESS_CONTEXT_MAX_CHARS`` (50,000) client-side BEFORE the HTTP call so callers fail fast and avoid a wasted round-trip to the server (which enforces the same limit and returns 400 above it). Then calls ``PUT /api/app/projects/{pid}/business-context`` (project) or ``PUT /api/app/organizations/{org_id}/business-context`` (org). The PUT is full-replace β€” pass an empty string to clear (or use ``clear_business_context`` for clarity). Args: content: New markdown content. Empty string clears the context at this scope. level: ``"project"`` (default) or ``"organization"``. organization_id: Optional explicit org ID, only honored when ``level="organization"``. Auto-resolved from ``/me`` when omitted. Returns: ``BusinessContext`` echoing the server's saved content. Raises: ValueError: ``level`` is not ``"organization"`` or ``"project"``. BusinessContextValidationError: ``len(content) > 50_000`` (client-side check, no HTTP call made). ConfigError: Credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Caller lacks ``edit_project_info`` permission (403) or other API error (400). ServerError: Server-side errors (5xx). WorkspaceScopeError: ``level="organization"`` and the org ID could not be auto-resolved. MixpanelHeadlessError: API response is missing the ``content`` field. Example: ```python ws = Workspace() ws.set_business_context("# Acme Corp\\n...", level="project") ws.set_business_context( "# Org-wide context", level="organization", ) ``` """ self._validate_level(level) if len(content) > BUSINESS_CONTEXT_MAX_CHARS: raise BusinessContextValidationError( f"content exceeds maximum length of " f"{BUSINESS_CONTEXT_MAX_CHARS} characters (got {len(content)})", details={ "length": len(content), "max": BUSINESS_CONTEXT_MAX_CHARS, }, ) client = self._require_api_client() if level == "organization": org_id = self._resolve_organization_id(organization_id) raw = client.set_business_context(content, organization_id=org_id) return BusinessContext( level="organization", content=self._require_str_field( raw, "content", method="set_business_context", ), organization_id=org_id, ) raw = client.set_business_context(content) return BusinessContext( level="project", content=self._require_str_field( raw, "content", method="set_business_context", ), project_id=self._session.project.id, ) ```` ### clear_business_context ``` clear_business_context( *, level: Literal["organization", "project"] = "project", organization_id: int | None = None, ) -> BusinessContext ``` Clear business context at the given scope. Convenience wrapper that calls `set_business_context("", level=..., organization_id=...)`. Useful for documenting intent β€” equivalent to passing an empty string explicitly. | PARAMETER | DESCRIPTION | | ----------------- | -------------------------------------------------------------------------------------------------------------- | | `level` | "project" (default) or "organization". **TYPE:** `Literal['organization', 'project']` **DEFAULT:** `'project'` | | `organization_id` | Optional explicit org ID for level="organization". **TYPE:** \`int | | RETURNS | DESCRIPTION | | ----------------- | -------------------------------------------------- | | `BusinessContext` | BusinessContext with content="" (the cleared state | | `BusinessContext` | echoed back from the server). | | RAISES | DESCRIPTION | | --------------------- | ------------------------------------------------------------------------- | | `ValueError` | level is not "organization" or "project". | | `ConfigError` | Credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Caller lacks edit_project_info permission (403) or other API error (400). | | `ServerError` | Server-side errors (5xx). | | `WorkspaceScopeError` | level="organization" and the org ID could not be auto-resolved. | Example ``` ws = Workspace() ws.clear_business_context(level="project") ``` Source code in `src/mixpanel_headless/workspace.py` ```` def clear_business_context( self, *, level: Literal["organization", "project"] = "project", organization_id: int | None = None, ) -> BusinessContext: """Clear business context at the given scope. Convenience wrapper that calls ``set_business_context("", level=..., organization_id=...)``. Useful for documenting intent β€” equivalent to passing an empty string explicitly. Args: level: ``"project"`` (default) or ``"organization"``. organization_id: Optional explicit org ID for ``level="organization"``. Returns: ``BusinessContext`` with ``content=""`` (the cleared state echoed back from the server). Raises: ValueError: ``level`` is not ``"organization"`` or ``"project"``. ConfigError: Credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Caller lacks ``edit_project_info`` permission (403) or other API error (400). ServerError: Server-side errors (5xx). WorkspaceScopeError: ``level="organization"`` and the org ID could not be auto-resolved. Example: ```python ws = Workspace() ws.clear_business_context(level="project") ``` """ return self.set_business_context( "", level=level, organization_id=organization_id, ) ```` ### get_business_context_chain ``` get_business_context_chain() -> BusinessContextChain ``` Read both organization and project business context together. Issues exactly one App API request to `GET /api/app/projects/{pid}/business-context/chain` β€” a server-side convenience that returns both scopes for the active project. `organization.organization_id` is populated on a best-effort basis from the cached `/me` response (in-memory or disk); when the cache is cold it is left as `None` rather than triggering an extra `/me` round-trip. Callers that need a guaranteed org ID should use `get_business_context(level= "organization")`, which performs full resolution. | RETURNS | DESCRIPTION | | ---------------------- | ---------------------------------------------------- | | `BusinessContextChain` | BusinessContextChain with populated organization and | | `BusinessContextChain` | project fields. Either content may be empty when no | | `BusinessContextChain` | context is set at that scope. | | `BusinessContextChain` | organization.organization_id may be None when the | | `BusinessContextChain` | /me cache is cold (see method description). | | RAISES | DESCRIPTION | | ----------------------- | ---------------------------------------------------------------- | | `ConfigError` | Credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Caller lacks project access (403, 404) or other API error (400). | | `ServerError` | Server-side errors (5xx). | | `MixpanelHeadlessError` | API response is missing org_context or project_context. | Example ``` ws = Workspace() chain = ws.get_business_context_chain() print("ORG:", chain.organization.content) print("PROJECT:", chain.project.content) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def get_business_context_chain(self) -> BusinessContextChain: """Read both organization and project business context together. Issues exactly one App API request to ``GET /api/app/projects/{pid}/business-context/chain`` β€” a server-side convenience that returns both scopes for the active project. ``organization.organization_id`` is populated on a best-effort basis from the cached ``/me`` response (in-memory or disk); when the cache is cold it is left as ``None`` rather than triggering an extra ``/me`` round-trip. Callers that need a guaranteed org ID should use ``get_business_context(level= "organization")``, which performs full resolution. Returns: ``BusinessContextChain`` with populated ``organization`` and ``project`` fields. Either ``content`` may be empty when no context is set at that scope. ``organization.organization_id`` may be ``None`` when the ``/me`` cache is cold (see method description). Raises: ConfigError: Credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Caller lacks project access (403, 404) or other API error (400). ServerError: Server-side errors (5xx). MixpanelHeadlessError: API response is missing ``org_context`` or ``project_context``. Example: ```python ws = Workspace() chain = ws.get_business_context_chain() print("ORG:", chain.organization.content) print("PROJECT:", chain.project.content) ``` """ client = self._require_api_client() raw = client.get_business_context_chain() org_content = self._require_str_field( raw, "org_context", method="get_business_context_chain", ) project_content = self._require_str_field( raw, "project_context", method="get_business_context_chain", ) org_id = self._cached_organization_id() return BusinessContextChain( organization=BusinessContext( level="organization", content=org_content, organization_id=org_id, ), project=BusinessContext( level="project", content=project_content, project_id=self._session.project.id, ), ) ```` ### query ``` query( events: str | Metric | CohortMetric | Formula | Sequence[str | Metric | CohortMetric | Formula], *, from_date: str | None = None, to_date: str | None = None, last: int = 30, unit: QueryTimeUnit = "day", math: MathType = "total", math_property: str | None = None, per_user: PerUserAggregation | None = None, percentile_value: int | float | None = None, group_by: str | GroupBy | CohortBreakdown | FrequencyBreakdown | list[str | GroupBy | CohortBreakdown | FrequencyBreakdown] | None = None, where: Filter | FrequencyFilter | list[Filter | FrequencyFilter] | None = None, formula: str | None = None, formula_label: str | None = None, rolling: int | None = None, cumulative: bool = False, mode: Literal["timeseries", "total", "table"] = "timeseries", time_comparison: TimeComparison | None = None, data_group_id: int | None = None, ) -> QueryResult ``` Run a typed insights query against the Mixpanel API. Generates bookmark params from keyword arguments, POSTs them inline to `/api/query/insights`, and returns a structured QueryResult with lazy DataFrame conversion. | PARAMETER | DESCRIPTION | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `events` | Event name(s) to query. Accepts a single string, a Metric object, a CohortMetric object, a Formula object, or a sequence mixing strings, Metrics, CohortMetrics, and Formulas. Formula objects in the list are extracted and appended as formula show clauses. When events includes a CohortMetric, math, math_property, and per_user are silently ignored for that entry β€” cohort size is always counted as unique users (CM3). **TYPE:** \`str | | `from_date` | Start date (YYYY-MM-DD). If set, overrides last. **TYPE:** \`str | | `to_date` | End date (YYYY-MM-DD). Requires from_date. **TYPE:** \`str | | `last` | Relative time range in days. Default: 30. Ignored if from_date is set. **TYPE:** `int` **DEFAULT:** `30` | | `unit` | Time aggregation unit. Default: "day". **TYPE:** `QueryTimeUnit` **DEFAULT:** `'day'` | | `math` | Aggregation function for plain-string events. Default: "total". **TYPE:** `MathType` **DEFAULT:** `'total'` | | `math_property` | Property name for property-based math (average, sum, percentiles). **TYPE:** \`str | | `per_user` | Per-user pre-aggregation (average, total, min, max). **TYPE:** \`PerUserAggregation | | `percentile_value` | Custom percentile value (e.g. 95 for p95). Required when math="percentile". Maps to percentile in bookmark measurement. Ignored for other math types. **TYPE:** \`int | | `group_by` | Break down results by property or cohort membership. Accepts a string, GroupBy, CohortBreakdown, or list of any mix. **TYPE:** \`str | | `where` | Filter results by conditions. Accepts a Filter or list of Filters. **TYPE:** \`Filter | | `formula` | Formula expression referencing events by position (A, B, C...). Requires 2+ events. Cannot be combined with Formula objects in events. **TYPE:** \`str | | `formula_label` | Display label for formula result. **TYPE:** \`str | | `rolling` | Rolling window size in periods. Mutually exclusive with cumulative. **TYPE:** \`int | | `cumulative` | Enable cumulative analysis mode. Mutually exclusive with rolling. **TYPE:** `bool` **DEFAULT:** `False` | | `mode` | Result shape. "timeseries" returns per-period data, "total" returns a single aggregate, "table" returns tabular data. Default: "timeseries". **TYPE:** `Literal['timeseries', 'total', 'table']` **DEFAULT:** `'timeseries'` | | `time_comparison` | Optional period-over-period comparison. Use TimeComparison.relative("month") for previous month, TimeComparison.absolute_start("2026-01-01") for a fixed start date, etc. Default: None. **TYPE:** \`TimeComparison | | `data_group_id` | Optional data group ID for group-level analytics. Scopes the query to a specific data group. Default: None. **TYPE:** \`int | | RETURNS | DESCRIPTION | | ------------- | ------------------------------------------------------ | | `QueryResult` | QueryResult with series data, DataFrame, and metadata. | | RAISES | DESCRIPTION | | --------------------- | -------------------------------------- | | `ValueError` | If arguments violate validation rules. | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials. | | `QueryError` | Invalid query parameters. | | `RateLimitError` | Rate limit exceeded. | Example ``` ws = Workspace() # Simple event query result = ws.query("Login") print(result.df.head()) # With aggregation and time range result = ws.query("Login", math="unique", last=7, unit="day") # Multi-event with formula (top-level parameter) result = ws.query( [Metric("Signup", math="unique"), Metric("Purchase", math="unique")], formula="(B / A) * 100", formula_label="Conversion Rate", ) # Multi-event with formula (Formula in list) result = ws.query( [Metric("Signup", math="unique"), Metric("Purchase", math="unique"), Formula("(B / A) * 100", label="Conversion Rate")], ) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def query( self, events: str | Metric | CohortMetric | Formula | Sequence[str | Metric | CohortMetric | Formula], *, from_date: str | None = None, to_date: str | None = None, last: int = 30, unit: QueryTimeUnit = "day", math: MathType = "total", math_property: str | None = None, per_user: PerUserAggregation | None = None, percentile_value: int | float | None = None, group_by: str | GroupBy | CohortBreakdown | FrequencyBreakdown | list[str | GroupBy | CohortBreakdown | FrequencyBreakdown] | None = None, where: Filter | FrequencyFilter | list[Filter | FrequencyFilter] | None = None, formula: str | None = None, formula_label: str | None = None, rolling: int | None = None, cumulative: bool = False, mode: Literal["timeseries", "total", "table"] = "timeseries", time_comparison: TimeComparison | None = None, data_group_id: int | None = None, ) -> QueryResult: """Run a typed insights query against the Mixpanel API. Generates bookmark params from keyword arguments, POSTs them inline to ``/api/query/insights``, and returns a structured QueryResult with lazy DataFrame conversion. Args: events: Event name(s) to query. Accepts a single string, a Metric object, a CohortMetric object, a Formula object, or a sequence mixing strings, Metrics, CohortMetrics, and Formulas. Formula objects in the list are extracted and appended as formula show clauses. When events includes a CohortMetric, ``math``, ``math_property``, and ``per_user`` are silently ignored for that entry β€” cohort size is always counted as unique users (CM3). from_date: Start date (YYYY-MM-DD). If set, overrides ``last``. to_date: End date (YYYY-MM-DD). Requires ``from_date``. last: Relative time range in days. Default: 30. Ignored if ``from_date`` is set. unit: Time aggregation unit. Default: ``"day"``. math: Aggregation function for plain-string events. Default: ``"total"``. math_property: Property name for property-based math (average, sum, percentiles). per_user: Per-user pre-aggregation (average, total, min, max). percentile_value: Custom percentile value (e.g. 95 for p95). Required when ``math="percentile"``. Maps to ``percentile`` in bookmark measurement. Ignored for other math types. group_by: Break down results by property or cohort membership. Accepts a string, ``GroupBy``, ``CohortBreakdown``, or list of any mix. where: Filter results by conditions. Accepts a Filter or list of Filters. formula: Formula expression referencing events by position (A, B, C...). Requires 2+ events. Cannot be combined with Formula objects in ``events``. formula_label: Display label for formula result. rolling: Rolling window size in periods. Mutually exclusive with ``cumulative``. cumulative: Enable cumulative analysis mode. Mutually exclusive with ``rolling``. mode: Result shape. ``"timeseries"`` returns per-period data, ``"total"`` returns a single aggregate, ``"table"`` returns tabular data. Default: ``"timeseries"``. time_comparison: Optional period-over-period comparison. Use ``TimeComparison.relative("month")`` for previous month, ``TimeComparison.absolute_start("2026-01-01")`` for a fixed start date, etc. Default: ``None``. data_group_id: Optional data group ID for group-level analytics. Scopes the query to a specific data group. Default: ``None``. Returns: QueryResult with series data, DataFrame, and metadata. Raises: ValueError: If arguments violate validation rules. ConfigError: If credentials are not available. AuthenticationError: Invalid credentials. QueryError: Invalid query parameters. RateLimitError: Rate limit exceeded. Example: ```python ws = Workspace() # Simple event query result = ws.query("Login") print(result.df.head()) # With aggregation and time range result = ws.query("Login", math="unique", last=7, unit="day") # Multi-event with formula (top-level parameter) result = ws.query( [Metric("Signup", math="unique"), Metric("Purchase", math="unique")], formula="(B / A) * 100", formula_label="Conversion Rate", ) # Multi-event with formula (Formula in list) result = ws.query( [Metric("Signup", math="unique"), Metric("Purchase", math="unique"), Formula("(B / A) * 100", label="Conversion Rate")], ) ``` """ params = self._resolve_and_build_params( events=events, from_date=from_date, to_date=to_date, last=last, unit=unit, math=math, math_property=math_property, per_user=per_user, percentile_value=percentile_value, group_by=group_by, where=where, formula=formula, formula_label=formula_label, rolling=rolling, cumulative=cumulative, mode=mode, time_comparison=time_comparison, data_group_id=data_group_id, ) return self._live_query_service.query( bookmark_params=params, project_id=int(self._session.project.id), ) ```` ### build_params ``` build_params( events: str | Metric | CohortMetric | Formula | Sequence[str | Metric | CohortMetric | Formula], *, from_date: str | None = None, to_date: str | None = None, last: int = 30, unit: QueryTimeUnit = "day", math: MathType = "total", math_property: str | None = None, per_user: PerUserAggregation | None = None, percentile_value: int | float | None = None, group_by: str | GroupBy | CohortBreakdown | FrequencyBreakdown | list[str | GroupBy | CohortBreakdown | FrequencyBreakdown] | None = None, where: Filter | FrequencyFilter | list[Filter | FrequencyFilter] | None = None, formula: str | None = None, formula_label: str | None = None, rolling: int | None = None, cumulative: bool = False, mode: Literal["timeseries", "total", "table"] = "timeseries", time_comparison: TimeComparison | None = None, data_group_id: int | None = None, ) -> dict[str, Any] ``` Build validated bookmark params without executing the API call. Has the same signature as :meth:`query` but returns the generated bookmark params dict instead of querying the Mixpanel API. Useful for debugging, inspecting generated JSON, persisting via :meth:`create_bookmark`, or testing. | PARAMETER | DESCRIPTION | | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `events` | Event name(s) to query. Accepts a single string, a Metric, CohortMetric, Formula, or a sequence mixing strings, Metrics, CohortMetrics, and Formulas. **TYPE:** \`str | | `from_date` | Start date (YYYY-MM-DD). If set, overrides last. **TYPE:** \`str | | `to_date` | End date (YYYY-MM-DD). Requires from_date. **TYPE:** \`str | | `last` | Relative time range in days. Default: 30. **TYPE:** `int` **DEFAULT:** `30` | | `unit` | Time aggregation unit. Default: "day". **TYPE:** `QueryTimeUnit` **DEFAULT:** `'day'` | | `math` | Aggregation function for plain-string events. Default: "total". **TYPE:** `MathType` **DEFAULT:** `'total'` | | `math_property` | Property name for property-based math. **TYPE:** \`str | | `per_user` | Per-user pre-aggregation. **TYPE:** \`PerUserAggregation | | `percentile_value` | Custom percentile value (e.g. 95). Required when math="percentile". **TYPE:** \`int | | `group_by` | Break down results by property or cohort membership. Accepts a string, GroupBy, CohortBreakdown, or list of any mix. **TYPE:** \`str | | `where` | Filter results by conditions. **TYPE:** \`Filter | | `formula` | Formula expression referencing events by position. **TYPE:** \`str | | `formula_label` | Display label for formula result. **TYPE:** \`str | | `rolling` | Rolling window size in periods. **TYPE:** \`int | | `cumulative` | Enable cumulative analysis mode. **TYPE:** `bool` **DEFAULT:** `False` | | `mode` | Result shape. Default: "timeseries". **TYPE:** `Literal['timeseries', 'total', 'table']` **DEFAULT:** `'timeseries'` | | `time_comparison` | Optional period-over-period comparison. Use TimeComparison.relative("month") for previous month, etc. Default: None. **TYPE:** \`TimeComparison | | `data_group_id` | Optional data group ID for group-level analytics. Scopes the query to a specific data group. Default: None. **TYPE:** \`int | | RETURNS | DESCRIPTION | | ---------------- | ----------------------------------------------------- | | `dict[str, Any]` | Bookmark params dict with sections and displayOptions | | `dict[str, Any]` | keys, ready for use with the insights API or | | `dict[str, Any]` | meth:create_bookmark. | | RAISES | DESCRIPTION | | ------------------------- | -------------------------------------- | | `BookmarkValidationError` | If arguments violate validation rules. | Example ``` ws = Workspace() # Inspect generated bookmark JSON params = ws.build_params("Login", math="unique", last=7) print(json.dumps(params, indent=2)) # Save as a bookmark (dashboard_id required) ws.create_bookmark(CreateBookmarkParams( name="Daily Unique Logins", bookmark_type="insights", params=params, dashboard_id=12345, )) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def build_params( self, events: str | Metric | CohortMetric | Formula | Sequence[str | Metric | CohortMetric | Formula], *, from_date: str | None = None, to_date: str | None = None, last: int = 30, unit: QueryTimeUnit = "day", math: MathType = "total", math_property: str | None = None, per_user: PerUserAggregation | None = None, percentile_value: int | float | None = None, group_by: str | GroupBy | CohortBreakdown | FrequencyBreakdown | list[str | GroupBy | CohortBreakdown | FrequencyBreakdown] | None = None, where: Filter | FrequencyFilter | list[Filter | FrequencyFilter] | None = None, formula: str | None = None, formula_label: str | None = None, rolling: int | None = None, cumulative: bool = False, mode: Literal["timeseries", "total", "table"] = "timeseries", time_comparison: TimeComparison | None = None, data_group_id: int | None = None, ) -> dict[str, Any]: """Build validated bookmark params without executing the API call. Has the same signature as :meth:`query` but returns the generated bookmark params dict instead of querying the Mixpanel API. Useful for debugging, inspecting generated JSON, persisting via :meth:`create_bookmark`, or testing. Args: events: Event name(s) to query. Accepts a single string, a ``Metric``, ``CohortMetric``, ``Formula``, or a sequence mixing strings, ``Metric``s, ``CohortMetric``s, and ``Formula``s. from_date: Start date (YYYY-MM-DD). If set, overrides ``last``. to_date: End date (YYYY-MM-DD). Requires ``from_date``. last: Relative time range in days. Default: 30. unit: Time aggregation unit. Default: ``"day"``. math: Aggregation function for plain-string events. Default: ``"total"``. math_property: Property name for property-based math. per_user: Per-user pre-aggregation. percentile_value: Custom percentile value (e.g. 95). Required when ``math="percentile"``. group_by: Break down results by property or cohort membership. Accepts a string, ``GroupBy``, ``CohortBreakdown``, or list of any mix. where: Filter results by conditions. formula: Formula expression referencing events by position. formula_label: Display label for formula result. rolling: Rolling window size in periods. cumulative: Enable cumulative analysis mode. mode: Result shape. Default: ``"timeseries"``. time_comparison: Optional period-over-period comparison. Use ``TimeComparison.relative("month")`` for previous month, etc. Default: ``None``. data_group_id: Optional data group ID for group-level analytics. Scopes the query to a specific data group. Default: ``None``. Returns: Bookmark params dict with ``sections`` and ``displayOptions`` keys, ready for use with the insights API or :meth:`create_bookmark`. Raises: BookmarkValidationError: If arguments violate validation rules. Example: ```python ws = Workspace() # Inspect generated bookmark JSON params = ws.build_params("Login", math="unique", last=7) print(json.dumps(params, indent=2)) # Save as a bookmark (dashboard_id required) ws.create_bookmark(CreateBookmarkParams( name="Daily Unique Logins", bookmark_type="insights", params=params, dashboard_id=12345, )) ``` """ return self._resolve_and_build_params( events=events, from_date=from_date, to_date=to_date, last=last, unit=unit, math=math, math_property=math_property, per_user=per_user, percentile_value=percentile_value, group_by=group_by, where=where, formula=formula, formula_label=formula_label, rolling=rolling, cumulative=cumulative, mode=mode, time_comparison=time_comparison, data_group_id=data_group_id, ) ```` ### query_funnel ``` query_funnel( steps: list[str | FunnelStep], *, conversion_window: int = 14, conversion_window_unit: Literal[ "second", "minute", "hour", "day", "week", "month", "session" ] = "day", order: Literal["loose", "any"] = "loose", from_date: str | None = None, to_date: str | None = None, last: int = 30, unit: QueryTimeUnit = "day", math: FunnelMathType = "conversion_rate_unique", math_property: str | None = None, group_by: str | GroupBy | CohortBreakdown | list[str | GroupBy | CohortBreakdown] | None = None, where: Filter | list[Filter] | None = None, exclusions: list[str | Exclusion] | None = None, holding_constant: str | HoldingConstant | list[str | HoldingConstant] | None = None, mode: Literal["steps", "trends", "table"] = "steps", reentry_mode: FunnelReentryMode | None = None, time_comparison: TimeComparison | None = None, data_group_id: int | None = None, ) -> FunnelQueryResult ``` Run a typed funnel query against the Mixpanel API. Generates funnel bookmark params from keyword arguments, POSTs them inline to `/api/query/insights`, and returns a structured FunnelQueryResult with lazy DataFrame conversion. | PARAMETER | DESCRIPTION | | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `steps` | Funnel step specifications. At least 2 required. Accepts event name strings or FunnelStep objects for per-step filters, labels, and ordering. **TYPE:** \`list\[str | | `conversion_window` | How long users have to complete the funnel. Default: 14. **TYPE:** `int` **DEFAULT:** `14` | | `conversion_window_unit` | Time unit for conversion window. Default: "day". **TYPE:** `Literal['second', 'minute', 'hour', 'day', 'week', 'month', 'session']` **DEFAULT:** `'day'` | | `order` | Step ordering mode. "loose" requires steps in order but allows other events between. "any" allows steps in any order. Default: "loose". **TYPE:** `Literal['loose', 'any']` **DEFAULT:** `'loose'` | | `from_date` | Start date (YYYY-MM-DD). If set, overrides last. **TYPE:** \`str | | `to_date` | End date (YYYY-MM-DD). Requires from_date. **TYPE:** \`str | | `last` | Relative time range in days. Default: 30. **TYPE:** `int` **DEFAULT:** `30` | | `unit` | Time aggregation unit. Default: "day". **TYPE:** `QueryTimeUnit` **DEFAULT:** `'day'` | | `math` | Funnel aggregation function. Default: "conversion_rate_unique". **TYPE:** `FunnelMathType` **DEFAULT:** `'conversion_rate_unique'` | | `math_property` | Numeric property name for property-aggregation math types ("average", "median", "min", "max", "p25", "p75", "p90", "p99"). Required when using those math types; must be None for count/rate math types. Default: None. **TYPE:** \`str | | `group_by` | Break down results by property or cohort membership. Accepts a string, GroupBy, CohortBreakdown, or list of any mix. **TYPE:** \`str | | `where` | Filter results by conditions. **TYPE:** \`Filter | | `exclusions` | Events to exclude between steps. Accepts event name strings or Exclusion objects. **TYPE:** \`list\[str | | `holding_constant` | Properties to hold constant across steps. Accepts strings, HoldingConstant objects, or a list mixing both. **TYPE:** \`str | | `mode` | Result display mode. "steps" shows step-level data, "trends" shows conversion over time, "table" shows tabular breakdown. Default: "steps". **TYPE:** `Literal['steps', 'trends', 'table']` **DEFAULT:** `'steps'` | | `reentry_mode` | Funnel reentry mode controlling how users re-enter the funnel after conversion. One of "default", "basic", "aggressive", or "optimized". Default: None (server default). **TYPE:** \`FunnelReentryMode | | `time_comparison` | Optional period-over-period comparison. Use TimeComparison.relative("month") for previous month, etc. Default: None. **TYPE:** \`TimeComparison | | `data_group_id` | Optional data group ID for group-level analytics. Scopes the query to a specific data group. Default: None. **TYPE:** \`int | | RETURNS | DESCRIPTION | | ------------------- | ---------------------------------------------------------- | | `FunnelQueryResult` | FunnelQueryResult with step data, DataFrame, and metadata. | | RAISES | DESCRIPTION | | ------------------------- | -------------------------------------------------------- | | `BookmarkValidationError` | If arguments violate validation rules (before API call). | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials. | | `QueryError` | Invalid query parameters. | | `RateLimitError` | Rate limit exceeded. | Example ``` ws = Workspace() # Simple two-step funnel result = ws.query_funnel(["Signup", "Purchase"]) print(result.overall_conversion_rate) # Configured funnel result = ws.query_funnel( ["Signup", "Add to Cart", "Checkout", "Purchase"], conversion_window=7, order="loose", last=90, ) print(result.df) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def query_funnel( self, steps: list[str | FunnelStep], *, conversion_window: int = 14, conversion_window_unit: Literal[ "second", "minute", "hour", "day", "week", "month", "session" ] = "day", order: Literal["loose", "any"] = "loose", from_date: str | None = None, to_date: str | None = None, last: int = 30, unit: QueryTimeUnit = "day", math: FunnelMathType = "conversion_rate_unique", math_property: str | None = None, group_by: str | GroupBy | CohortBreakdown | list[str | GroupBy | CohortBreakdown] | None = None, where: Filter | list[Filter] | None = None, exclusions: list[str | Exclusion] | None = None, holding_constant: ( str | HoldingConstant | list[str | HoldingConstant] | None ) = None, mode: Literal["steps", "trends", "table"] = "steps", reentry_mode: FunnelReentryMode | None = None, time_comparison: TimeComparison | None = None, data_group_id: int | None = None, ) -> FunnelQueryResult: """Run a typed funnel query against the Mixpanel API. Generates funnel bookmark params from keyword arguments, POSTs them inline to ``/api/query/insights``, and returns a structured FunnelQueryResult with lazy DataFrame conversion. Args: steps: Funnel step specifications. At least 2 required. Accepts event name strings or ``FunnelStep`` objects for per-step filters, labels, and ordering. conversion_window: How long users have to complete the funnel. Default: 14. conversion_window_unit: Time unit for conversion window. Default: ``"day"``. order: Step ordering mode. ``"loose"`` requires steps in order but allows other events between. ``"any"`` allows steps in any order. Default: ``"loose"``. from_date: Start date (YYYY-MM-DD). If set, overrides ``last``. to_date: End date (YYYY-MM-DD). Requires ``from_date``. last: Relative time range in days. Default: 30. unit: Time aggregation unit. Default: ``"day"``. math: Funnel aggregation function. Default: ``"conversion_rate_unique"``. math_property: Numeric property name for property-aggregation math types (``"average"``, ``"median"``, ``"min"``, ``"max"``, ``"p25"``, ``"p75"``, ``"p90"``, ``"p99"``). Required when using those math types; must be ``None`` for count/rate math types. Default: ``None``. group_by: Break down results by property or cohort membership. Accepts a string, ``GroupBy``, ``CohortBreakdown``, or list of any mix. where: Filter results by conditions. exclusions: Events to exclude between steps. Accepts event name strings or ``Exclusion`` objects. holding_constant: Properties to hold constant across steps. Accepts strings, ``HoldingConstant`` objects, or a list mixing both. mode: Result display mode. ``"steps"`` shows step-level data, ``"trends"`` shows conversion over time, ``"table"`` shows tabular breakdown. Default: ``"steps"``. reentry_mode: Funnel reentry mode controlling how users re-enter the funnel after conversion. One of ``"default"``, ``"basic"``, ``"aggressive"``, or ``"optimized"``. Default: ``None`` (server default). time_comparison: Optional period-over-period comparison. Use ``TimeComparison.relative("month")`` for previous month, etc. Default: ``None``. data_group_id: Optional data group ID for group-level analytics. Scopes the query to a specific data group. Default: ``None``. Returns: FunnelQueryResult with step data, DataFrame, and metadata. Raises: BookmarkValidationError: If arguments violate validation rules (before API call). ConfigError: If credentials are not available. AuthenticationError: Invalid credentials. QueryError: Invalid query parameters. RateLimitError: Rate limit exceeded. Example: ```python ws = Workspace() # Simple two-step funnel result = ws.query_funnel(["Signup", "Purchase"]) print(result.overall_conversion_rate) # Configured funnel result = ws.query_funnel( ["Signup", "Add to Cart", "Checkout", "Purchase"], conversion_window=7, order="loose", last=90, ) print(result.df) ``` """ params = self._resolve_and_build_funnel_params( steps=steps, conversion_window=conversion_window, conversion_window_unit=conversion_window_unit, order=order, math=math, math_property=math_property, from_date=from_date, to_date=to_date, last=last, unit=unit, group_by=group_by, where=where, exclusions=exclusions, holding_constant=holding_constant, mode=mode, reentry_mode=reentry_mode, time_comparison=time_comparison, data_group_id=data_group_id, ) return self._live_query_service.query_funnel( bookmark_params=params, project_id=int(self._session.project.id), ) ```` ### build_funnel_params ``` build_funnel_params( steps: list[str | FunnelStep], *, conversion_window: int = 14, conversion_window_unit: Literal[ "second", "minute", "hour", "day", "week", "month", "session" ] = "day", order: Literal["loose", "any"] = "loose", from_date: str | None = None, to_date: str | None = None, last: int = 30, unit: QueryTimeUnit = "day", math: FunnelMathType = "conversion_rate_unique", math_property: str | None = None, group_by: str | GroupBy | CohortBreakdown | list[str | GroupBy | CohortBreakdown] | None = None, where: Filter | list[Filter] | None = None, exclusions: list[str | Exclusion] | None = None, holding_constant: str | HoldingConstant | list[str | HoldingConstant] | None = None, mode: Literal["steps", "trends", "table"] = "steps", reentry_mode: FunnelReentryMode | None = None, time_comparison: TimeComparison | None = None, data_group_id: int | None = None, ) -> dict[str, Any] ``` Build validated funnel bookmark params without executing. Has the same signature as :meth:`query_funnel` but returns the generated bookmark params dict instead of querying the API. Useful for debugging, inspecting generated JSON, persisting via :meth:`create_bookmark`, or testing. | PARAMETER | DESCRIPTION | | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `steps` | Funnel step specifications. At least 2 required. **TYPE:** \`list\[str | | `conversion_window` | Conversion window size. Default: 14. **TYPE:** `int` **DEFAULT:** `14` | | `conversion_window_unit` | Time unit. Default: "day". **TYPE:** `Literal['second', 'minute', 'hour', 'day', 'week', 'month', 'session']` **DEFAULT:** `'day'` | | `order` | Step ordering mode. Default: "loose". **TYPE:** `Literal['loose', 'any']` **DEFAULT:** `'loose'` | | `from_date` | Start date (YYYY-MM-DD) or None. **TYPE:** \`str | | `to_date` | End date (YYYY-MM-DD) or None. **TYPE:** \`str | | `last` | Relative time range in days. Default: 30. **TYPE:** `int` **DEFAULT:** `30` | | `unit` | Time aggregation unit. Default: "day". **TYPE:** `QueryTimeUnit` **DEFAULT:** `'day'` | | `math` | Aggregation function. Default: "conversion_rate_unique". **TYPE:** `FunnelMathType` **DEFAULT:** `'conversion_rate_unique'` | | `math_property` | Numeric property name for property-aggregation math types. Required for "average", "median", etc. Default: None. **TYPE:** \`str | | `group_by` | Break down results by property or cohort membership. Accepts a string, GroupBy, CohortBreakdown, or list of any mix. **TYPE:** \`str | | `where` | Filter results by conditions. **TYPE:** \`Filter | | `exclusions` | Events to exclude between steps. **TYPE:** \`list\[str | | `holding_constant` | Properties to hold constant. **TYPE:** \`str | | `mode` | Display mode. Default: "steps". **TYPE:** `Literal['steps', 'trends', 'table']` **DEFAULT:** `'steps'` | | `reentry_mode` | Funnel reentry mode controlling how users re-enter the funnel after conversion. One of "default", "basic", "aggressive", or "optimized". Default: None (server default). **TYPE:** \`FunnelReentryMode | | `time_comparison` | Optional period-over-period comparison. Use TimeComparison.relative("month") for previous month, etc. Default: None. **TYPE:** \`TimeComparison | | `data_group_id` | Optional data group ID for group-level analytics. Scopes the query to a specific data group. Default: None. **TYPE:** \`int | | RETURNS | DESCRIPTION | | ---------------- | -------------------------------------- | | `dict[str, Any]` | Bookmark params dict with sections and | | `dict[str, Any]` | displayOptions keys. | | RAISES | DESCRIPTION | | ------------------------- | -------------------------------------- | | `BookmarkValidationError` | If arguments violate validation rules. | Example ``` ws = Workspace() # Inspect generated JSON params = ws.build_funnel_params(["Signup", "Purchase"]) print(json.dumps(params, indent=2)) # Save as a report (dashboard_id required) ws.create_bookmark(CreateBookmarkParams( name="Signup β†’ Purchase Funnel", bookmark_type="funnels", params=params, dashboard_id=12345, )) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def build_funnel_params( self, steps: list[str | FunnelStep], *, conversion_window: int = 14, conversion_window_unit: Literal[ "second", "minute", "hour", "day", "week", "month", "session" ] = "day", order: Literal["loose", "any"] = "loose", from_date: str | None = None, to_date: str | None = None, last: int = 30, unit: QueryTimeUnit = "day", math: FunnelMathType = "conversion_rate_unique", math_property: str | None = None, group_by: str | GroupBy | CohortBreakdown | list[str | GroupBy | CohortBreakdown] | None = None, where: Filter | list[Filter] | None = None, exclusions: list[str | Exclusion] | None = None, holding_constant: ( str | HoldingConstant | list[str | HoldingConstant] | None ) = None, mode: Literal["steps", "trends", "table"] = "steps", reentry_mode: FunnelReentryMode | None = None, time_comparison: TimeComparison | None = None, data_group_id: int | None = None, ) -> dict[str, Any]: """Build validated funnel bookmark params without executing. Has the same signature as :meth:`query_funnel` but returns the generated bookmark params dict instead of querying the API. Useful for debugging, inspecting generated JSON, persisting via :meth:`create_bookmark`, or testing. Args: steps: Funnel step specifications. At least 2 required. conversion_window: Conversion window size. Default: 14. conversion_window_unit: Time unit. Default: ``"day"``. order: Step ordering mode. Default: ``"loose"``. from_date: Start date (YYYY-MM-DD) or None. to_date: End date (YYYY-MM-DD) or None. last: Relative time range in days. Default: 30. unit: Time aggregation unit. Default: ``"day"``. math: Aggregation function. Default: ``"conversion_rate_unique"``. math_property: Numeric property name for property-aggregation math types. Required for ``"average"``, ``"median"``, etc. Default: ``None``. group_by: Break down results by property or cohort membership. Accepts a string, ``GroupBy``, ``CohortBreakdown``, or list of any mix. where: Filter results by conditions. exclusions: Events to exclude between steps. holding_constant: Properties to hold constant. mode: Display mode. Default: ``"steps"``. reentry_mode: Funnel reentry mode controlling how users re-enter the funnel after conversion. One of ``"default"``, ``"basic"``, ``"aggressive"``, or ``"optimized"``. Default: ``None`` (server default). time_comparison: Optional period-over-period comparison. Use ``TimeComparison.relative("month")`` for previous month, etc. Default: ``None``. data_group_id: Optional data group ID for group-level analytics. Scopes the query to a specific data group. Default: ``None``. Returns: Bookmark params dict with ``sections`` and ``displayOptions`` keys. Raises: BookmarkValidationError: If arguments violate validation rules. Example: ```python ws = Workspace() # Inspect generated JSON params = ws.build_funnel_params(["Signup", "Purchase"]) print(json.dumps(params, indent=2)) # Save as a report (dashboard_id required) ws.create_bookmark(CreateBookmarkParams( name="Signup β†’ Purchase Funnel", bookmark_type="funnels", params=params, dashboard_id=12345, )) ``` """ return self._resolve_and_build_funnel_params( steps=steps, conversion_window=conversion_window, conversion_window_unit=conversion_window_unit, order=order, math=math, math_property=math_property, from_date=from_date, to_date=to_date, last=last, unit=unit, group_by=group_by, where=where, exclusions=exclusions, holding_constant=holding_constant, mode=mode, reentry_mode=reentry_mode, time_comparison=time_comparison, data_group_id=data_group_id, ) ```` ### query_retention ``` query_retention( born_event: str | RetentionEvent, return_event: str | RetentionEvent, *, retention_unit: TimeUnit = "week", alignment: RetentionAlignment = "birth", bucket_sizes: list[int] | None = None, from_date: str | None = None, to_date: str | None = None, last: int = 30, unit: QueryTimeUnit = "day", math: RetentionMathType = "retention_rate", group_by: str | GroupBy | CohortBreakdown | list[str | GroupBy | CohortBreakdown] | None = None, where: Filter | list[Filter] | None = None, mode: RetentionMode = "curve", unbounded_mode: RetentionUnboundedMode | None = None, retention_cumulative: bool = False, time_comparison: TimeComparison | None = None, data_group_id: int | None = None, ) -> RetentionQueryResult ``` Run a typed retention query against the Mixpanel API. Generates retention bookmark params from keyword arguments, POSTs them inline to `/api/query/insights`, and returns a structured RetentionQueryResult with lazy DataFrame conversion. | PARAMETER | DESCRIPTION | | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `born_event` | Event that defines cohort membership. Accepts an event name string or a RetentionEvent object for per-event filters. **TYPE:** \`str | | `return_event` | Event that defines return. Accepts an event name string or a RetentionEvent object. **TYPE:** \`str | | `retention_unit` | Retention period unit. Default: "week". **TYPE:** `TimeUnit` **DEFAULT:** `'week'` | | `alignment` | Retention alignment mode. Default: "birth". **TYPE:** `RetentionAlignment` **DEFAULT:** `'birth'` | | `bucket_sizes` | Custom bucket sizes (positive ints in ascending order). Default: None (uniform buckets). **TYPE:** \`list[int] | | `from_date` | Start date (YYYY-MM-DD). If set, overrides last. **TYPE:** \`str | | `to_date` | End date (YYYY-MM-DD). Requires from_date. **TYPE:** \`str | | `last` | Relative time range in days. Default: 30. **TYPE:** `int` **DEFAULT:** `30` | | `unit` | Time aggregation unit (day, week, or month β€” hour is not supported for retention). Default: "day". **TYPE:** `QueryTimeUnit` **DEFAULT:** `'day'` | | `math` | Retention aggregation function. Default: "retention_rate". **TYPE:** `RetentionMathType` **DEFAULT:** `'retention_rate'` | | `group_by` | Break down results by property or cohort membership. Accepts a string, GroupBy, CohortBreakdown, or list of any mix. **TYPE:** \`str | | `where` | Filter results by conditions. **TYPE:** \`Filter | | `mode` | Result display mode. Default: "curve". **TYPE:** `RetentionMode` **DEFAULT:** `'curve'` | | `unbounded_mode` | Retention unbounded mode controlling how retention is counted in unbounded periods. One of "none", "carry_back", "carry_forward", or "consecutive_forward". Default: None (server default). **TYPE:** \`RetentionUnboundedMode | | `retention_cumulative` | Whether to use cumulative retention counting. Default: False. **TYPE:** `bool` **DEFAULT:** `False` | | `time_comparison` | Optional period-over-period comparison. Use TimeComparison.relative("month") for previous month, etc. Default: None. **TYPE:** \`TimeComparison | | `data_group_id` | Optional data group ID for group-level analytics. Scopes the query to a specific data group. Default: None. **TYPE:** \`int | | RETURNS | DESCRIPTION | | ---------------------- | ----------------------------------------------------- | | `RetentionQueryResult` | RetentionQueryResult with cohort data, DataFrame, and | | `RetentionQueryResult` | metadata. | | RAISES | DESCRIPTION | | ------------------------- | -------------------------------------------------------- | | `BookmarkValidationError` | If arguments violate validation rules (before API call). | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials. | | `QueryError` | Invalid query parameters. | | `RateLimitError` | Rate limit exceeded. | Example ``` ws = Workspace() # Simple retention query result = ws.query_retention("Signup", "Login") print(result.average) # With configuration result = ws.query_retention( "Signup", "Login", retention_unit="day", bucket_sizes=[1, 3, 7, 14, 30], last=90, ) print(result.df) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def query_retention( self, born_event: str | RetentionEvent, return_event: str | RetentionEvent, *, retention_unit: TimeUnit = "week", alignment: RetentionAlignment = "birth", bucket_sizes: list[int] | None = None, from_date: str | None = None, to_date: str | None = None, last: int = 30, unit: QueryTimeUnit = "day", math: RetentionMathType = "retention_rate", group_by: str | GroupBy | CohortBreakdown | list[str | GroupBy | CohortBreakdown] | None = None, where: Filter | list[Filter] | None = None, mode: RetentionMode = "curve", unbounded_mode: RetentionUnboundedMode | None = None, retention_cumulative: bool = False, time_comparison: TimeComparison | None = None, data_group_id: int | None = None, ) -> RetentionQueryResult: """Run a typed retention query against the Mixpanel API. Generates retention bookmark params from keyword arguments, POSTs them inline to ``/api/query/insights``, and returns a structured RetentionQueryResult with lazy DataFrame conversion. Args: born_event: Event that defines cohort membership. Accepts an event name string or a ``RetentionEvent`` object for per-event filters. return_event: Event that defines return. Accepts an event name string or a ``RetentionEvent`` object. retention_unit: Retention period unit. Default: ``"week"``. alignment: Retention alignment mode. Default: ``"birth"``. bucket_sizes: Custom bucket sizes (positive ints in ascending order). Default: ``None`` (uniform buckets). from_date: Start date (YYYY-MM-DD). If set, overrides ``last``. to_date: End date (YYYY-MM-DD). Requires ``from_date``. last: Relative time range in days. Default: 30. unit: Time aggregation unit (``day``, ``week``, or ``month`` β€” ``hour`` is not supported for retention). Default: ``"day"``. math: Retention aggregation function. Default: ``"retention_rate"``. group_by: Break down results by property or cohort membership. Accepts a string, ``GroupBy``, ``CohortBreakdown``, or list of any mix. where: Filter results by conditions. mode: Result display mode. Default: ``"curve"``. unbounded_mode: Retention unbounded mode controlling how retention is counted in unbounded periods. One of ``"none"``, ``"carry_back"``, ``"carry_forward"``, or ``"consecutive_forward"``. Default: ``None`` (server default). retention_cumulative: Whether to use cumulative retention counting. Default: ``False``. time_comparison: Optional period-over-period comparison. Use ``TimeComparison.relative("month")`` for previous month, etc. Default: ``None``. data_group_id: Optional data group ID for group-level analytics. Scopes the query to a specific data group. Default: ``None``. Returns: RetentionQueryResult with cohort data, DataFrame, and metadata. Raises: BookmarkValidationError: If arguments violate validation rules (before API call). ConfigError: If credentials are not available. AuthenticationError: Invalid credentials. QueryError: Invalid query parameters. RateLimitError: Rate limit exceeded. Example: ```python ws = Workspace() # Simple retention query result = ws.query_retention("Signup", "Login") print(result.average) # With configuration result = ws.query_retention( "Signup", "Login", retention_unit="day", bucket_sizes=[1, 3, 7, 14, 30], last=90, ) print(result.df) ``` """ params = self._resolve_and_build_retention_params( born_event=born_event, return_event=return_event, retention_unit=retention_unit, alignment=alignment, bucket_sizes=bucket_sizes, math=math, from_date=from_date, to_date=to_date, last=last, unit=unit, group_by=group_by, where=where, mode=mode, unbounded_mode=unbounded_mode, retention_cumulative=retention_cumulative, time_comparison=time_comparison, data_group_id=data_group_id, ) return self._live_query_service.query_retention( bookmark_params=params, project_id=int(self._session.project.id), ) ```` ### build_retention_params ``` build_retention_params( born_event: str | RetentionEvent, return_event: str | RetentionEvent, *, retention_unit: TimeUnit = "week", alignment: RetentionAlignment = "birth", bucket_sizes: list[int] | None = None, from_date: str | None = None, to_date: str | None = None, last: int = 30, unit: QueryTimeUnit = "day", math: RetentionMathType = "retention_rate", group_by: str | GroupBy | CohortBreakdown | list[str | GroupBy | CohortBreakdown] | None = None, where: Filter | list[Filter] | None = None, mode: RetentionMode = "curve", unbounded_mode: RetentionUnboundedMode | None = None, retention_cumulative: bool = False, time_comparison: TimeComparison | None = None, data_group_id: int | None = None, ) -> dict[str, Any] ``` Build validated retention bookmark params without executing. Accepts the same arguments as :meth:`query_retention` but returns the generated bookmark params `dict` (not a `RetentionQueryResult`) instead of querying the API. Useful for debugging, inspecting generated JSON, persisting via :meth:`create_bookmark`, or testing. | PARAMETER | DESCRIPTION | | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `born_event` | Event that defines cohort membership. **TYPE:** \`str | | `return_event` | Event that defines return. **TYPE:** \`str | | `retention_unit` | Retention period unit. Default: "week". **TYPE:** `TimeUnit` **DEFAULT:** `'week'` | | `alignment` | Retention alignment mode. Default: "birth". **TYPE:** `RetentionAlignment` **DEFAULT:** `'birth'` | | `bucket_sizes` | Custom bucket sizes. Default: None. **TYPE:** \`list[int] | | `from_date` | Start date (YYYY-MM-DD) or None. **TYPE:** \`str | | `to_date` | End date (YYYY-MM-DD) or None. **TYPE:** \`str | | `last` | Relative time range in days. Default: 30. **TYPE:** `int` **DEFAULT:** `30` | | `unit` | Time aggregation unit (day, week, or month β€” hour is not supported for retention). Default: "day". **TYPE:** `QueryTimeUnit` **DEFAULT:** `'day'` | | `math` | Aggregation function. Default: "retention_rate". **TYPE:** `RetentionMathType` **DEFAULT:** `'retention_rate'` | | `group_by` | Break down results by property or cohort membership. Accepts a string, GroupBy, CohortBreakdown, or list of any mix. **TYPE:** \`str | | `where` | Filter results by conditions. **TYPE:** \`Filter | | `mode` | Display mode. Default: "curve". **TYPE:** `RetentionMode` **DEFAULT:** `'curve'` | | `unbounded_mode` | Retention unbounded mode controlling how retention is counted in unbounded periods. One of "none", "carry_back", "carry_forward", or "consecutive_forward". Default: None (server default). **TYPE:** \`RetentionUnboundedMode | | `retention_cumulative` | Whether to use cumulative retention counting. Default: False. **TYPE:** `bool` **DEFAULT:** `False` | | `time_comparison` | Optional period-over-period comparison. Use TimeComparison.relative("month") for previous month, etc. Default: None. **TYPE:** \`TimeComparison | | `data_group_id` | Optional data group ID for group-level analytics. Scopes the query to a specific data group. Default: None. **TYPE:** \`int | | RETURNS | DESCRIPTION | | ---------------- | -------------------------------------- | | `dict[str, Any]` | Bookmark params dict with sections and | | `dict[str, Any]` | displayOptions keys. | | RAISES | DESCRIPTION | | ------------------------- | -------------------------------------- | | `BookmarkValidationError` | If arguments violate validation rules. | Example ``` ws = Workspace() # Inspect generated JSON params = ws.build_retention_params("Signup", "Login") print(json.dumps(params, indent=2)) # Save as a report (dashboard_id required) ws.create_bookmark(CreateBookmarkParams( name="Signup β†’ Login Retention", bookmark_type="retention", params=params, dashboard_id=12345, )) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def build_retention_params( self, born_event: str | RetentionEvent, return_event: str | RetentionEvent, *, retention_unit: TimeUnit = "week", alignment: RetentionAlignment = "birth", bucket_sizes: list[int] | None = None, from_date: str | None = None, to_date: str | None = None, last: int = 30, unit: QueryTimeUnit = "day", math: RetentionMathType = "retention_rate", group_by: str | GroupBy | CohortBreakdown | list[str | GroupBy | CohortBreakdown] | None = None, where: Filter | list[Filter] | None = None, mode: RetentionMode = "curve", unbounded_mode: RetentionUnboundedMode | None = None, retention_cumulative: bool = False, time_comparison: TimeComparison | None = None, data_group_id: int | None = None, ) -> dict[str, Any]: """Build validated retention bookmark params without executing. Accepts the same arguments as :meth:`query_retention` but returns the generated bookmark params ``dict`` (not a ``RetentionQueryResult``) instead of querying the API. Useful for debugging, inspecting generated JSON, persisting via :meth:`create_bookmark`, or testing. Args: born_event: Event that defines cohort membership. return_event: Event that defines return. retention_unit: Retention period unit. Default: ``"week"``. alignment: Retention alignment mode. Default: ``"birth"``. bucket_sizes: Custom bucket sizes. Default: ``None``. from_date: Start date (YYYY-MM-DD) or None. to_date: End date (YYYY-MM-DD) or None. last: Relative time range in days. Default: 30. unit: Time aggregation unit (``day``, ``week``, or ``month`` β€” ``hour`` is not supported for retention). Default: ``"day"``. math: Aggregation function. Default: ``"retention_rate"``. group_by: Break down results by property or cohort membership. Accepts a string, ``GroupBy``, ``CohortBreakdown``, or list of any mix. where: Filter results by conditions. mode: Display mode. Default: ``"curve"``. unbounded_mode: Retention unbounded mode controlling how retention is counted in unbounded periods. One of ``"none"``, ``"carry_back"``, ``"carry_forward"``, or ``"consecutive_forward"``. Default: ``None`` (server default). retention_cumulative: Whether to use cumulative retention counting. Default: ``False``. time_comparison: Optional period-over-period comparison. Use ``TimeComparison.relative("month")`` for previous month, etc. Default: ``None``. data_group_id: Optional data group ID for group-level analytics. Scopes the query to a specific data group. Default: ``None``. Returns: Bookmark params dict with ``sections`` and ``displayOptions`` keys. Raises: BookmarkValidationError: If arguments violate validation rules. Example: ```python ws = Workspace() # Inspect generated JSON params = ws.build_retention_params("Signup", "Login") print(json.dumps(params, indent=2)) # Save as a report (dashboard_id required) ws.create_bookmark(CreateBookmarkParams( name="Signup β†’ Login Retention", bookmark_type="retention", params=params, dashboard_id=12345, )) ``` """ return self._resolve_and_build_retention_params( born_event=born_event, return_event=return_event, retention_unit=retention_unit, alignment=alignment, bucket_sizes=bucket_sizes, math=math, from_date=from_date, to_date=to_date, last=last, unit=unit, group_by=group_by, where=where, mode=mode, unbounded_mode=unbounded_mode, retention_cumulative=retention_cumulative, time_comparison=time_comparison, data_group_id=data_group_id, ) ```` ### query_flow ``` query_flow( event: str | FlowStep | Sequence[str | FlowStep], *, forward: int = 3, reverse: int = 0, from_date: str | None = None, to_date: str | None = None, last: int = 30, conversion_window: int = 7, conversion_window_unit: Literal["day", "week", "month", "session"] = "day", count_type: Literal["unique", "total", "session"] = "unique", cardinality: int = 3, collapse_repeated: bool = False, hidden_events: list[str] | None = None, mode: Literal["sankey", "paths", "tree"] = "sankey", where: Filter | list[Filter] | None = None, data_group_id: int | None = None, segments: str | GroupBy | CohortBreakdown | FrequencyBreakdown | list[str | GroupBy | CohortBreakdown | FrequencyBreakdown] | None = None, exclusions: list[str] | None = None, ) -> FlowQueryResult ``` Run a typed flow query against the Mixpanel API. Generates flow bookmark params from keyword arguments, POSTs them inline to `/arb_funnels`, and returns a structured `FlowQueryResult` with lazy DataFrame conversion. | PARAMETER | DESCRIPTION | | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `event` | Event specification. Accepts an event name string, a FlowStep object for per-step configuration, or a list of strings/FlowStep objects for multi-step flows. **TYPE:** \`str | | `forward` | Default number of forward steps to trace from each anchor event. Overridden by per-step values. Default: 3. **TYPE:** `int` **DEFAULT:** `3` | | `reverse` | Default number of reverse steps to trace from each anchor event. Overridden by per-step values. Default: 0. **TYPE:** `int` **DEFAULT:** `0` | | `from_date` | Start date (YYYY-MM-DD). If set, overrides last. **TYPE:** \`str | | `to_date` | End date (YYYY-MM-DD). Requires from_date. **TYPE:** \`str | | `last` | Relative time range in days. Default: 30. **TYPE:** `int` **DEFAULT:** `30` | | `conversion_window` | Conversion window size. Default: 7. **TYPE:** `int` **DEFAULT:** `7` | | `conversion_window_unit` | Conversion window unit. Default: "day". **TYPE:** `Literal['day', 'week', 'month', 'session']` **DEFAULT:** `'day'` | | `count_type` | Counting method for flow analysis. Default: "unique". **TYPE:** `Literal['unique', 'total', 'session']` **DEFAULT:** `'unique'` | | `cardinality` | Number of top paths to display. Default: 3. **TYPE:** `int` **DEFAULT:** `3` | | `collapse_repeated` | Whether to merge consecutive repeated events. Default: False. **TYPE:** `bool` **DEFAULT:** `False` | | `hidden_events` | Events to hide from the flow visualization. Default: None. **TYPE:** \`list[str] | | `mode` | Flow visualization mode. Default: "sankey". **TYPE:** `Literal['sankey', 'paths', 'tree']` **DEFAULT:** `'sankey'` | | `where` | Filter results by cohort membership or property conditions. Cohort filters (Filter.in_cohort / Filter.not_in_cohort) produce filter_by_cohort. Property filters (Filter.equals, etc.) produce filter_by_event. Default: None. **TYPE:** \`Filter | | `data_group_id` | Optional data group ID for group-level analytics. Scopes the query to a specific data group. Default: None. **TYPE:** \`int | | `segments` | Segment (breakdown) specification for flow results. Accepts a string, GroupBy, or list of strings/GroupBy objects. Default: None. **TYPE:** \`str | | `exclusions` | List of event names to exclude from flow paths. Default: None. **TYPE:** \`list[str] | | RETURNS | DESCRIPTION | | ----------------- | -------------------------------------------------- | | `FlowQueryResult` | FlowQueryResult with steps, flows, breakdowns, and | | `FlowQueryResult` | metadata. | | RAISES | DESCRIPTION | | ------------------------- | -------------------------------------------------------- | | `BookmarkValidationError` | If arguments violate validation rules (before API call). | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials. | | `QueryError` | Invalid query parameters. | | `RateLimitError` | Rate limit exceeded. | Example ``` ws = Workspace() # Simple flow query result = ws.query_flow("Login") print(result.overall_conversion_rate) # With configuration result = ws.query_flow( FlowStep("Login", forward=5, reverse=2), mode="paths", last=90, ) print(result.df) # With property filter and segments result = ws.query_flow( "Login", where=Filter.equals("country", "US"), segments=GroupBy("platform"), exclusions=["Error Event"], ) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def query_flow( self, event: str | FlowStep | Sequence[str | FlowStep], *, forward: int = 3, reverse: int = 0, from_date: str | None = None, to_date: str | None = None, last: int = 30, conversion_window: int = 7, conversion_window_unit: Literal["day", "week", "month", "session"] = "day", count_type: Literal["unique", "total", "session"] = "unique", cardinality: int = 3, collapse_repeated: bool = False, hidden_events: list[str] | None = None, mode: Literal["sankey", "paths", "tree"] = "sankey", where: Filter | list[Filter] | None = None, data_group_id: int | None = None, segments: str | GroupBy | CohortBreakdown | FrequencyBreakdown | list[str | GroupBy | CohortBreakdown | FrequencyBreakdown] | None = None, exclusions: list[str] | None = None, ) -> FlowQueryResult: """Run a typed flow query against the Mixpanel API. Generates flow bookmark params from keyword arguments, POSTs them inline to ``/arb_funnels``, and returns a structured ``FlowQueryResult`` with lazy DataFrame conversion. Args: event: Event specification. Accepts an event name string, a ``FlowStep`` object for per-step configuration, or a list of strings/``FlowStep`` objects for multi-step flows. forward: Default number of forward steps to trace from each anchor event. Overridden by per-step values. Default: ``3``. reverse: Default number of reverse steps to trace from each anchor event. Overridden by per-step values. Default: ``0``. from_date: Start date (YYYY-MM-DD). If set, overrides ``last``. to_date: End date (YYYY-MM-DD). Requires ``from_date``. last: Relative time range in days. Default: 30. conversion_window: Conversion window size. Default: 7. conversion_window_unit: Conversion window unit. Default: ``"day"``. count_type: Counting method for flow analysis. Default: ``"unique"``. cardinality: Number of top paths to display. Default: ``3``. collapse_repeated: Whether to merge consecutive repeated events. Default: ``False``. hidden_events: Events to hide from the flow visualization. Default: ``None``. mode: Flow visualization mode. Default: ``"sankey"``. where: Filter results by cohort membership or property conditions. Cohort filters (``Filter.in_cohort`` / ``Filter.not_in_cohort``) produce ``filter_by_cohort``. Property filters (``Filter.equals``, etc.) produce ``filter_by_event``. Default: ``None``. data_group_id: Optional data group ID for group-level analytics. Scopes the query to a specific data group. Default: ``None``. segments: Segment (breakdown) specification for flow results. Accepts a string, ``GroupBy``, or list of strings/``GroupBy`` objects. Default: ``None``. exclusions: List of event names to exclude from flow paths. Default: ``None``. Returns: FlowQueryResult with steps, flows, breakdowns, and metadata. Raises: BookmarkValidationError: If arguments violate validation rules (before API call). ConfigError: If credentials are not available. AuthenticationError: Invalid credentials. QueryError: Invalid query parameters. RateLimitError: Rate limit exceeded. Example: ```python ws = Workspace() # Simple flow query result = ws.query_flow("Login") print(result.overall_conversion_rate) # With configuration result = ws.query_flow( FlowStep("Login", forward=5, reverse=2), mode="paths", last=90, ) print(result.df) # With property filter and segments result = ws.query_flow( "Login", where=Filter.equals("country", "US"), segments=GroupBy("platform"), exclusions=["Error Event"], ) ``` """ params = self._resolve_and_build_flow_params( event=event, forward=forward, reverse=reverse, from_date=from_date, to_date=to_date, last=last, conversion_window=conversion_window, conversion_window_unit=conversion_window_unit, count_type=count_type, cardinality=cardinality, collapse_repeated=collapse_repeated, hidden_events=hidden_events, mode=mode, where=where, data_group_id=data_group_id, segments=segments, exclusions=exclusions, ) return self._live_query_service.query_flow( bookmark_params=params, project_id=int(self._session.project.id), mode=mode, ) ```` ### build_flow_params ``` build_flow_params( event: str | FlowStep | Sequence[str | FlowStep], *, forward: int = 3, reverse: int = 0, from_date: str | None = None, to_date: str | None = None, last: int = 30, conversion_window: int = 7, conversion_window_unit: Literal["day", "week", "month", "session"] = "day", count_type: Literal["unique", "total", "session"] = "unique", cardinality: int = 3, collapse_repeated: bool = False, hidden_events: list[str] | None = None, mode: Literal["sankey", "paths", "tree"] = "sankey", where: Filter | list[Filter] | None = None, data_group_id: int | None = None, segments: str | GroupBy | CohortBreakdown | FrequencyBreakdown | list[str | GroupBy | CohortBreakdown | FrequencyBreakdown] | None = None, exclusions: list[str] | None = None, ) -> dict[str, Any] ``` Build validated flow bookmark params without executing. Accepts the same arguments as :meth:`query_flow` but returns the generated bookmark params `dict` instead of querying the API. Useful for debugging, inspecting generated JSON, persisting via :meth:`create_bookmark`, or testing. | PARAMETER | DESCRIPTION | | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `event` | Event specification. Accepts an event name string, a FlowStep object, or a list of strings/FlowStep objects. **TYPE:** \`str | | `forward` | Default forward step count. Default: 3. **TYPE:** `int` **DEFAULT:** `3` | | `reverse` | Default reverse step count. Default: 0. **TYPE:** `int` **DEFAULT:** `0` | | `from_date` | Start date (YYYY-MM-DD) or None. **TYPE:** \`str | | `to_date` | End date (YYYY-MM-DD) or None. **TYPE:** \`str | | `last` | Relative time range in days. Default: 30. **TYPE:** `int` **DEFAULT:** `30` | | `conversion_window` | Conversion window size. Default: 7. **TYPE:** `int` **DEFAULT:** `7` | | `conversion_window_unit` | Conversion window unit. Default: "day". **TYPE:** `Literal['day', 'week', 'month', 'session']` **DEFAULT:** `'day'` | | `count_type` | Counting method. Default: "unique". **TYPE:** `Literal['unique', 'total', 'session']` **DEFAULT:** `'unique'` | | `cardinality` | Number of top paths. Default: 3. **TYPE:** `int` **DEFAULT:** `3` | | `collapse_repeated` | Merge repeated events. Default: False. **TYPE:** `bool` **DEFAULT:** `False` | | `hidden_events` | Events to hide. Default: None. **TYPE:** \`list[str] | | `mode` | Display mode. Default: "sankey". **TYPE:** `Literal['sankey', 'paths', 'tree']` **DEFAULT:** `'sankey'` | | `where` | Filter results by cohort membership or property conditions. Cohort filters produce filter_by_cohort, property filters produce filter_by_event. Default: None. **TYPE:** \`Filter | | `data_group_id` | Optional data group ID for group-level analytics. Scopes the query to a specific data group. Default: None. **TYPE:** \`int | | `segments` | Segment (breakdown) specification for flow results. Accepts a string, GroupBy, or list of strings/GroupBy objects. Default: None. **TYPE:** \`str | | `exclusions` | List of event names to exclude from flow paths. Default: None. **TYPE:** \`list[str] | | RETURNS | DESCRIPTION | | ---------------- | ------------------------------------------------- | | `dict[str, Any]` | Flat bookmark params dict with steps, date_range, | | `dict[str, Any]` | chartType, count_type, and version keys. | | RAISES | DESCRIPTION | | ------------------------- | -------------------------------------- | | `BookmarkValidationError` | If arguments violate validation rules. | Example ``` ws = Workspace() # Inspect generated JSON params = ws.build_flow_params("Login") print(json.dumps(params, indent=2)) # With segments and exclusions params = ws.build_flow_params( "Login", segments=GroupBy("country"), exclusions=["Error Event"], where=Filter.equals("platform", "iOS"), ) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def build_flow_params( self, event: str | FlowStep | Sequence[str | FlowStep], *, forward: int = 3, reverse: int = 0, from_date: str | None = None, to_date: str | None = None, last: int = 30, conversion_window: int = 7, conversion_window_unit: Literal["day", "week", "month", "session"] = "day", count_type: Literal["unique", "total", "session"] = "unique", cardinality: int = 3, collapse_repeated: bool = False, hidden_events: list[str] | None = None, mode: Literal["sankey", "paths", "tree"] = "sankey", where: Filter | list[Filter] | None = None, data_group_id: int | None = None, segments: str | GroupBy | CohortBreakdown | FrequencyBreakdown | list[str | GroupBy | CohortBreakdown | FrequencyBreakdown] | None = None, exclusions: list[str] | None = None, ) -> dict[str, Any]: """Build validated flow bookmark params without executing. Accepts the same arguments as :meth:`query_flow` but returns the generated bookmark params ``dict`` instead of querying the API. Useful for debugging, inspecting generated JSON, persisting via :meth:`create_bookmark`, or testing. Args: event: Event specification. Accepts an event name string, a ``FlowStep`` object, or a list of strings/``FlowStep`` objects. forward: Default forward step count. Default: ``3``. reverse: Default reverse step count. Default: ``0``. from_date: Start date (YYYY-MM-DD) or ``None``. to_date: End date (YYYY-MM-DD) or ``None``. last: Relative time range in days. Default: 30. conversion_window: Conversion window size. Default: 7. conversion_window_unit: Conversion window unit. Default: ``"day"``. count_type: Counting method. Default: ``"unique"``. cardinality: Number of top paths. Default: ``3``. collapse_repeated: Merge repeated events. Default: ``False``. hidden_events: Events to hide. Default: ``None``. mode: Display mode. Default: ``"sankey"``. where: Filter results by cohort membership or property conditions. Cohort filters produce ``filter_by_cohort``, property filters produce ``filter_by_event``. Default: ``None``. data_group_id: Optional data group ID for group-level analytics. Scopes the query to a specific data group. Default: ``None``. segments: Segment (breakdown) specification for flow results. Accepts a string, ``GroupBy``, or list of strings/``GroupBy`` objects. Default: ``None``. exclusions: List of event names to exclude from flow paths. Default: ``None``. Returns: Flat bookmark params dict with ``steps``, ``date_range``, ``chartType``, ``count_type``, and ``version`` keys. Raises: BookmarkValidationError: If arguments violate validation rules. Example: ```python ws = Workspace() # Inspect generated JSON params = ws.build_flow_params("Login") print(json.dumps(params, indent=2)) # With segments and exclusions params = ws.build_flow_params( "Login", segments=GroupBy("country"), exclusions=["Error Event"], where=Filter.equals("platform", "iOS"), ) ``` """ return self._resolve_and_build_flow_params( event=event, forward=forward, reverse=reverse, from_date=from_date, to_date=to_date, last=last, conversion_window=conversion_window, conversion_window_unit=conversion_window_unit, count_type=count_type, cardinality=cardinality, collapse_repeated=collapse_repeated, hidden_events=hidden_events, mode=mode, where=where, data_group_id=data_group_id, segments=segments, exclusions=exclusions, ) ```` ### query_user ``` query_user( *, where: Filter | list[Filter] | str | None = None, cohort: int | CohortDefinition | None = None, properties: list[str] | None = None, sort_by: str | None = None, sort_order: Literal["ascending", "descending"] = "descending", limit: int | None = 1, search: str | None = None, distinct_id: str | None = None, distinct_ids: list[str] | None = None, group_id: str | None = None, as_of: str | int | None = None, mode: Literal["profiles", "aggregate"] = "aggregate", aggregate: Literal[ "count", "extremes", "percentile", "numeric_summary" ] = "count", aggregate_property: str | None = None, percentile: float | None = None, segment_by: list[int] | None = None, parallel: bool = False, workers: int = 5, include_all_users: bool = False, ) -> UserQueryResult ``` Query user profiles from Mixpanel's Engage API. Provides a high-level interface to Mixpanel's Engage API for querying user profiles with typed filters, cohort membership, sorting, and pagination. Results are returned as a structured `UserQueryResult` with lazy DataFrame conversion. | PARAMETER | DESCRIPTION | | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `where` | Filter profiles by property values. Accepts a single Filter, a list of Filter objects (AND-combined), a raw selector string, or None. **TYPE:** \`Filter | | `cohort` | Filter by cohort membership. An int for a saved cohort ID, or a CohortDefinition for an inline cohort definition. **TYPE:** \`int | | `properties` | Output properties to include in results. **TYPE:** \`list[str] | | `sort_by` | Property name to sort results by. **TYPE:** \`str | | `sort_order` | Sort direction ("ascending" or "descending"). **TYPE:** `Literal['ascending', 'descending']` **DEFAULT:** `'descending'` | | `limit` | Maximum profiles to return. Defaults to 1 for quick exploration. Use None to fetch all matching profiles. **TYPE:** \`int | | `search` | Full-text search term applied to profile properties. **TYPE:** \`str | | `distinct_id` | Look up a single user by distinct ID. **TYPE:** \`str | | `distinct_ids` | Batch look up multiple users by distinct IDs. **TYPE:** \`list[str] | | `group_id` | Query group profiles instead of user profiles. **TYPE:** \`str | | `as_of` | Point-in-time query. An ISO date string (YYYY-MM-DD) is converted to a Unix timestamp; an int is passed through directly. **TYPE:** \`str | | `mode` | Output mode ("profiles" or "aggregate"). **TYPE:** `Literal['profiles', 'aggregate']` **DEFAULT:** `'aggregate'` | | `aggregate` | Aggregation function for aggregate mode. One of "count" (profile count), "extremes" (min/max), "percentile" (Nth percentile), or "numeric_summary" (count/mean/var/sum_of_squares). **TYPE:** `Literal['count', 'extremes', 'percentile', 'numeric_summary']` **DEFAULT:** `'count'` | | `aggregate_property` | Property to aggregate on (required for non-count aggregations). **TYPE:** \`str | | `percentile` | Percentile value (0-100 exclusive). Required when aggregate="percentile". **TYPE:** \`float | | `segment_by` | Cohort IDs for segmented aggregation. **TYPE:** \`list[int] | | `parallel` | Whether to enable concurrent page fetching. **TYPE:** `bool` **DEFAULT:** `False` | | `workers` | Maximum concurrent workers for parallel fetching. **TYPE:** `int` **DEFAULT:** `5` | | `include_all_users` | Include non-members in cohort query results. **TYPE:** `bool` **DEFAULT:** `False` | | RETURNS | DESCRIPTION | | ----------------- | ------------------------------------------------------ | | `UserQueryResult` | UserQueryResult with profiles, total count, DataFrame, | | `UserQueryResult` | and execution metadata. | | RAISES | DESCRIPTION | | ------------------------- | --------------------------------- | | `BookmarkValidationError` | If any validation rule fails. | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `RateLimitError` | API rate limit exceeded (429). | | `APIError` | Other API communication errors. | Example ``` ws = Workspace() # Quick peek at one profile result = ws.query_user() print(result.df) # Filter premium users, sorted by LTV result = ws.query_user( where=Filter.equals("plan", "premium"), sort_by="ltv", sort_order="descending", limit=100, ) print(f"Total premium users: {result.total}") print(result.df.head()) # Batch lookup specific users result = ws.query_user( distinct_ids=["user_001", "user_002"], limit=None, ) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def query_user( self, *, where: Filter | list[Filter] | str | None = None, cohort: int | CohortDefinition | None = None, properties: list[str] | None = None, sort_by: str | None = None, sort_order: Literal["ascending", "descending"] = "descending", limit: int | None = 1, search: str | None = None, distinct_id: str | None = None, distinct_ids: list[str] | None = None, group_id: str | None = None, as_of: str | int | None = None, mode: Literal["profiles", "aggregate"] = "aggregate", aggregate: Literal[ "count", "extremes", "percentile", "numeric_summary" ] = "count", aggregate_property: str | None = None, percentile: float | None = None, segment_by: list[int] | None = None, parallel: bool = False, workers: int = 5, include_all_users: bool = False, ) -> UserQueryResult: """Query user profiles from Mixpanel's Engage API. Provides a high-level interface to Mixpanel's Engage API for querying user profiles with typed filters, cohort membership, sorting, and pagination. Results are returned as a structured ``UserQueryResult`` with lazy DataFrame conversion. Args: where: Filter profiles by property values. Accepts a single ``Filter``, a list of ``Filter`` objects (AND-combined), a raw selector string, or ``None``. cohort: Filter by cohort membership. An ``int`` for a saved cohort ID, or a ``CohortDefinition`` for an inline cohort definition. properties: Output properties to include in results. sort_by: Property name to sort results by. sort_order: Sort direction (``"ascending"`` or ``"descending"``). limit: Maximum profiles to return. Defaults to ``1`` for quick exploration. Use ``None`` to fetch all matching profiles. search: Full-text search term applied to profile properties. distinct_id: Look up a single user by distinct ID. distinct_ids: Batch look up multiple users by distinct IDs. group_id: Query group profiles instead of user profiles. as_of: Point-in-time query. An ISO date string (``YYYY-MM-DD``) is converted to a Unix timestamp; an ``int`` is passed through directly. mode: Output mode (``"profiles"`` or ``"aggregate"``). aggregate: Aggregation function for aggregate mode. One of ``"count"`` (profile count), ``"extremes"`` (min/max), ``"percentile"`` (Nth percentile), or ``"numeric_summary"`` (count/mean/var/sum_of_squares). aggregate_property: Property to aggregate on (required for non-count aggregations). percentile: Percentile value (0-100 exclusive). Required when ``aggregate="percentile"``. segment_by: Cohort IDs for segmented aggregation. parallel: Whether to enable concurrent page fetching. workers: Maximum concurrent workers for parallel fetching. include_all_users: Include non-members in cohort query results. Returns: ``UserQueryResult`` with profiles, total count, DataFrame, and execution metadata. Raises: BookmarkValidationError: If any validation rule fails. ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). RateLimitError: API rate limit exceeded (429). APIError: Other API communication errors. Example: ```python ws = Workspace() # Quick peek at one profile result = ws.query_user() print(result.df) # Filter premium users, sorted by LTV result = ws.query_user( where=Filter.equals("plan", "premium"), sort_by="ltv", sort_order="descending", limit=100, ) print(f"Total premium users: {result.total}") print(result.df.head()) # Batch lookup specific users result = ws.query_user( distinct_ids=["user_001", "user_002"], limit=None, ) ``` """ params = self._resolve_and_build_user_params( where=where, cohort=cohort, properties=properties, sort_by=sort_by, sort_order=sort_order, limit=limit, search=search, distinct_id=distinct_id, distinct_ids=distinct_ids, group_id=group_id, as_of=as_of, mode=mode, aggregate=aggregate, aggregate_property=aggregate_property, percentile=percentile, segment_by=segment_by, parallel=parallel, workers=workers, include_all_users=include_all_users, ) # Route by mode if mode == "aggregate": aggregate_data, total, computed_at, meta = self._execute_user_aggregate( params ) return UserQueryResult( computed_at=computed_at, total=total, profiles=[], params=params, meta=meta, mode="aggregate", aggregate_data=aggregate_data, ) # Profiles mode β€” choose sequential or parallel if parallel and limit != 1: profiles, total, computed_at, meta = self._execute_user_query_parallel( params, limit, workers ) else: if parallel and limit == 1: logger.debug("parallel=True ignored: limit=1 uses sequential path") profiles, total, computed_at, meta = self._execute_user_query_sequential( params, limit ) return UserQueryResult( computed_at=computed_at, total=total, profiles=profiles, params=params, meta=meta, mode="profiles", aggregate_data=None, ) ```` ### build_user_params ``` build_user_params( *, where: Filter | list[Filter] | str | None = None, cohort: int | CohortDefinition | None = None, properties: list[str] | None = None, sort_by: str | None = None, sort_order: Literal["ascending", "descending"] = "descending", search: str | None = None, distinct_id: str | None = None, distinct_ids: list[str] | None = None, group_id: str | None = None, as_of: str | int | None = None, mode: Literal["profiles", "aggregate"] = "aggregate", aggregate: Literal[ "count", "extremes", "percentile", "numeric_summary" ] = "count", aggregate_property: str | None = None, percentile: float | None = None, segment_by: list[int] | None = None, limit: int | None = 1, parallel: bool = False, workers: int = 5, include_all_users: bool = False, ) -> dict[str, Any] ``` Build engage API params without executing a query. Validates arguments and constructs the params dict that would be sent to the Engage API, without actually making an API call. Useful for debugging, testing, and inspecting the generated params before execution. | PARAMETER | DESCRIPTION | | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `where` | Filter profiles by property values. Accepts a single Filter, a list of Filter objects (AND-combined), a raw selector string, or None. **TYPE:** \`Filter | | `cohort` | Filter by cohort membership. An int for a saved cohort ID, or a CohortDefinition for an inline cohort definition. **TYPE:** \`int | | `properties` | Output properties to include in results. **TYPE:** \`list[str] | | `sort_by` | Property name to sort results by. **TYPE:** \`str | | `sort_order` | Sort direction ("ascending" or "descending"). **TYPE:** `Literal['ascending', 'descending']` **DEFAULT:** `'descending'` | | `search` | Full-text search term applied to profile properties. **TYPE:** \`str | | `distinct_id` | Look up a single user by distinct ID. **TYPE:** \`str | | `distinct_ids` | Batch look up multiple users by distinct IDs. **TYPE:** \`list[str] | | `group_id` | Query group profiles instead of user profiles. **TYPE:** \`str | | `as_of` | Point-in-time query. An ISO date string (YYYY-MM-DD) is converted to a Unix timestamp; an int is passed through directly. **TYPE:** \`str | | `mode` | Output mode ("profiles" or "aggregate"). **TYPE:** `Literal['profiles', 'aggregate']` **DEFAULT:** `'aggregate'` | | `aggregate` | Aggregation function for aggregate mode. One of "count" (profile count), "extremes" (min/max), "percentile" (Nth percentile), or "numeric_summary" (count/mean/var/sum_of_squares). **TYPE:** `Literal['count', 'extremes', 'percentile', 'numeric_summary']` **DEFAULT:** `'count'` | | `aggregate_property` | Property to aggregate on (required for non-count aggregations). **TYPE:** \`str | | `percentile` | Percentile value (0-100 exclusive). Required when aggregate="percentile". **TYPE:** \`float | | `segment_by` | Cohort IDs for segmented aggregation. **TYPE:** \`list[int] | | `limit` | Maximum profiles to return. Defaults to 1. Used for argument-level validation (U3); not included in the returned params dict. **TYPE:** \`int | | `parallel` | Whether to enable concurrent page fetching. Accepted for signature compatibility with query_user() but has no effect on the returned params dict. **TYPE:** `bool` **DEFAULT:** `False` | | `workers` | Maximum concurrent workers for parallel fetching. Accepted for signature compatibility with query_user() but has no effect on the returned params dict. **TYPE:** `int` **DEFAULT:** `5` | | `include_all_users` | Include non-members in cohort query results. **TYPE:** `bool` **DEFAULT:** `False` | | RETURNS | DESCRIPTION | | ---------------- | ---------------------------------------------------------- | | `dict[str, Any]` | Engage API params dict. Does not include pagination params | | `dict[str, Any]` | (page, session_id) or limit, which are added at | | `dict[str, Any]` | execution time by query_user(). | | RAISES | DESCRIPTION | | ------------------------- | ------------------------------------------------------------------------------------------------ | | `BookmarkValidationError` | If any validation rule fails at either the argument level (U1-U28) or the param level (UP1-UP4). | Example ``` ws = Workspace() params = ws.build_user_params( where=Filter.equals("plan", "premium"), sort_by="ltv", ) print(params) # {"where": 'properties["plan"] == "premium"', # "sort_key": 'properties["ltv"]', # "sort_order": "descending"} ``` Source code in `src/mixpanel_headless/workspace.py` ```` def build_user_params( self, *, where: Filter | list[Filter] | str | None = None, cohort: int | CohortDefinition | None = None, properties: list[str] | None = None, sort_by: str | None = None, sort_order: Literal["ascending", "descending"] = "descending", search: str | None = None, distinct_id: str | None = None, distinct_ids: list[str] | None = None, group_id: str | None = None, as_of: str | int | None = None, mode: Literal["profiles", "aggregate"] = "aggregate", aggregate: Literal[ "count", "extremes", "percentile", "numeric_summary" ] = "count", aggregate_property: str | None = None, percentile: float | None = None, segment_by: list[int] | None = None, limit: int | None = 1, parallel: bool = False, workers: int = 5, include_all_users: bool = False, ) -> dict[str, Any]: """Build engage API params without executing a query. Validates arguments and constructs the params dict that would be sent to the Engage API, without actually making an API call. Useful for debugging, testing, and inspecting the generated params before execution. Args: where: Filter profiles by property values. Accepts a single ``Filter``, a list of ``Filter`` objects (AND-combined), a raw selector string, or ``None``. cohort: Filter by cohort membership. An ``int`` for a saved cohort ID, or a ``CohortDefinition`` for an inline cohort definition. properties: Output properties to include in results. sort_by: Property name to sort results by. sort_order: Sort direction (``"ascending"`` or ``"descending"``). search: Full-text search term applied to profile properties. distinct_id: Look up a single user by distinct ID. distinct_ids: Batch look up multiple users by distinct IDs. group_id: Query group profiles instead of user profiles. as_of: Point-in-time query. An ISO date string (``YYYY-MM-DD``) is converted to a Unix timestamp; an ``int`` is passed through directly. mode: Output mode (``"profiles"`` or ``"aggregate"``). aggregate: Aggregation function for aggregate mode. One of ``"count"`` (profile count), ``"extremes"`` (min/max), ``"percentile"`` (Nth percentile), or ``"numeric_summary"`` (count/mean/var/sum_of_squares). aggregate_property: Property to aggregate on (required for non-count aggregations). percentile: Percentile value (0-100 exclusive). Required when ``aggregate="percentile"``. segment_by: Cohort IDs for segmented aggregation. limit: Maximum profiles to return. Defaults to ``1``. Used for argument-level validation (U3); not included in the returned params dict. parallel: Whether to enable concurrent page fetching. Accepted for signature compatibility with ``query_user()`` but has no effect on the returned params dict. workers: Maximum concurrent workers for parallel fetching. Accepted for signature compatibility with ``query_user()`` but has no effect on the returned params dict. include_all_users: Include non-members in cohort query results. Returns: Engage API params dict. Does not include pagination params (``page``, ``session_id``) or ``limit``, which are added at execution time by ``query_user()``. Raises: BookmarkValidationError: If any validation rule fails at either the argument level (U1-U28) or the param level (UP1-UP4). Example: ```python ws = Workspace() params = ws.build_user_params( where=Filter.equals("plan", "premium"), sort_by="ltv", ) print(params) # {"where": 'properties["plan"] == "premium"', # "sort_key": 'properties["ltv"]', # "sort_order": "descending"} ``` """ return self._resolve_and_build_user_params( where=where, cohort=cohort, properties=properties, sort_by=sort_by, sort_order=sort_order, search=search, distinct_id=distinct_id, distinct_ids=distinct_ids, group_id=group_id, as_of=as_of, mode=mode, aggregate=aggregate, aggregate_property=aggregate_property, percentile=percentile, segment_by=segment_by, limit=limit, parallel=parallel, workers=workers, include_all_users=include_all_users, ) ```` ### segmentation ``` segmentation( event: str, *, from_date: str, to_date: str, on: str | None = None, unit: Literal["day", "week", "month"] = "day", where: str | None = None, ) -> SegmentationResult ``` Run a segmentation query against Mixpanel API. | PARAMETER | DESCRIPTION | | ----------- | ------------------------------------------------------------------------------------------- | | `event` | Event name to query. **TYPE:** `str` | | `from_date` | Start date (YYYY-MM-DD). **TYPE:** `str` | | `to_date` | End date (YYYY-MM-DD). **TYPE:** `str` | | `on` | Optional property to segment by. **TYPE:** \`str | | `unit` | Time unit for aggregation. **TYPE:** `Literal['day', 'week', 'month']` **DEFAULT:** `'day'` | | `where` | Optional WHERE clause. **TYPE:** \`str | | RETURNS | DESCRIPTION | | -------------------- | ----------------------------------------- | | `SegmentationResult` | SegmentationResult with time-series data. | | RAISES | DESCRIPTION | | ------------- | --------------------------------- | | `ConfigError` | If API credentials not available. | Source code in `src/mixpanel_headless/workspace.py` ``` def segmentation( self, event: str, *, from_date: str, to_date: str, on: str | None = None, unit: Literal["day", "week", "month"] = "day", where: str | None = None, ) -> SegmentationResult: """Run a segmentation query against Mixpanel API. Args: event: Event name to query. from_date: Start date (YYYY-MM-DD). to_date: End date (YYYY-MM-DD). on: Optional property to segment by. unit: Time unit for aggregation. where: Optional WHERE clause. Returns: SegmentationResult with time-series data. Raises: ConfigError: If API credentials not available. """ return self._live_query_service.segmentation( event=event, from_date=from_date, to_date=to_date, on=on, unit=unit, where=where, ) ``` ### funnel ``` funnel( funnel_id: int, *, from_date: str, to_date: str, unit: str | None = None, on: str | None = None, ) -> FunnelResult ``` Run a funnel analysis query. | PARAMETER | DESCRIPTION | | ----------- | ------------------------------------------------ | | `funnel_id` | ID of saved funnel. **TYPE:** `int` | | `from_date` | Start date (YYYY-MM-DD). **TYPE:** `str` | | `to_date` | End date (YYYY-MM-DD). **TYPE:** `str` | | `unit` | Optional time unit. **TYPE:** \`str | | `on` | Optional property to segment by. **TYPE:** \`str | | RETURNS | DESCRIPTION | | -------------- | ---------------------------------------- | | `FunnelResult` | FunnelResult with step conversion rates. | | RAISES | DESCRIPTION | | ------------- | --------------------------------- | | `ConfigError` | If API credentials not available. | Source code in `src/mixpanel_headless/workspace.py` ``` def funnel( self, funnel_id: int, *, from_date: str, to_date: str, unit: str | None = None, on: str | None = None, ) -> FunnelResult: """Run a funnel analysis query. Args: funnel_id: ID of saved funnel. from_date: Start date (YYYY-MM-DD). to_date: End date (YYYY-MM-DD). unit: Optional time unit. on: Optional property to segment by. Returns: FunnelResult with step conversion rates. Raises: ConfigError: If API credentials not available. """ return self._live_query_service.funnel( funnel_id=funnel_id, from_date=from_date, to_date=to_date, unit=unit, on=on, ) ``` ### retention ``` retention( *, born_event: str, return_event: str, from_date: str, to_date: str, born_where: str | None = None, return_where: str | None = None, interval: int = 1, interval_count: int = 10, unit: Literal["day", "week", "month"] = "day", ) -> RetentionResult ``` Run a retention analysis query. | PARAMETER | DESCRIPTION | | ---------------- | --------------------------------------------------------------------------- | | `born_event` | Event that defines cohort entry. **TYPE:** `str` | | `return_event` | Event that defines return. **TYPE:** `str` | | `from_date` | Start date (YYYY-MM-DD). **TYPE:** `str` | | `to_date` | End date (YYYY-MM-DD). **TYPE:** `str` | | `born_where` | Optional filter for born event. **TYPE:** \`str | | `return_where` | Optional filter for return event. **TYPE:** \`str | | `interval` | Retention interval. **TYPE:** `int` **DEFAULT:** `1` | | `interval_count` | Number of intervals. **TYPE:** `int` **DEFAULT:** `10` | | `unit` | Time unit. **TYPE:** `Literal['day', 'week', 'month']` **DEFAULT:** `'day'` | | RETURNS | DESCRIPTION | | ----------------- | ------------------------------------------- | | `RetentionResult` | RetentionResult with cohort retention data. | | RAISES | DESCRIPTION | | ------------- | --------------------------------- | | `ConfigError` | If API credentials not available. | Source code in `src/mixpanel_headless/workspace.py` ``` def retention( self, *, born_event: str, return_event: str, from_date: str, to_date: str, born_where: str | None = None, return_where: str | None = None, interval: int = 1, interval_count: int = 10, unit: Literal["day", "week", "month"] = "day", ) -> RetentionResult: """Run a retention analysis query. Args: born_event: Event that defines cohort entry. return_event: Event that defines return. from_date: Start date (YYYY-MM-DD). to_date: End date (YYYY-MM-DD). born_where: Optional filter for born event. return_where: Optional filter for return event. interval: Retention interval. interval_count: Number of intervals. unit: Time unit. Returns: RetentionResult with cohort retention data. Raises: ConfigError: If API credentials not available. """ return self._live_query_service.retention( born_event=born_event, return_event=return_event, from_date=from_date, to_date=to_date, born_where=born_where, return_where=return_where, interval=interval, interval_count=interval_count, unit=unit, ) ``` ### jql ``` jql(script: str, params: dict[str, Any] | None = None) -> JQLResult ``` Execute a custom JQL script. | PARAMETER | DESCRIPTION | | --------- | ----------------------------------------------------------------- | | `script` | JQL script code. **TYPE:** `str` | | `params` | Optional parameters to pass to script. **TYPE:** \`dict[str, Any] | | RETURNS | DESCRIPTION | | ----------- | --------------------------------- | | `JQLResult` | JQLResult with raw query results. | | RAISES | DESCRIPTION | | ---------------- | --------------------------------- | | `ConfigError` | If API credentials not available. | | `JQLSyntaxError` | If script has syntax errors. | Source code in `src/mixpanel_headless/workspace.py` ``` def jql(self, script: str, params: dict[str, Any] | None = None) -> JQLResult: """Execute a custom JQL script. Args: script: JQL script code. params: Optional parameters to pass to script. Returns: JQLResult with raw query results. Raises: ConfigError: If API credentials not available. JQLSyntaxError: If script has syntax errors. """ return self._live_query_service.jql(script=script, params=params) ``` ### event_counts ``` event_counts( events: list[str], *, from_date: str, to_date: str, type: Literal["general", "unique", "average"] = "general", unit: Literal["day", "week", "month"] = "day", ) -> EventCountsResult ``` Get event counts for multiple events. | PARAMETER | DESCRIPTION | | ----------- | --------------------------------------------------------------------------------------------- | | `events` | List of event names. **TYPE:** `list[str]` | | `from_date` | Start date (YYYY-MM-DD). **TYPE:** `str` | | `to_date` | End date (YYYY-MM-DD). **TYPE:** `str` | | `type` | Counting method. **TYPE:** `Literal['general', 'unique', 'average']` **DEFAULT:** `'general'` | | `unit` | Time unit. **TYPE:** `Literal['day', 'week', 'month']` **DEFAULT:** `'day'` | | RETURNS | DESCRIPTION | | ------------------- | --------------------------------------------- | | `EventCountsResult` | EventCountsResult with time-series per event. | | RAISES | DESCRIPTION | | ------------- | --------------------------------- | | `ConfigError` | If API credentials not available. | Source code in `src/mixpanel_headless/workspace.py` ``` def event_counts( self, events: list[str], *, from_date: str, to_date: str, type: Literal["general", "unique", "average"] = "general", unit: Literal["day", "week", "month"] = "day", ) -> EventCountsResult: """Get event counts for multiple events. Args: events: List of event names. from_date: Start date (YYYY-MM-DD). to_date: End date (YYYY-MM-DD). type: Counting method. unit: Time unit. Returns: EventCountsResult with time-series per event. Raises: ConfigError: If API credentials not available. """ return self._live_query_service.event_counts( events=events, from_date=from_date, to_date=to_date, type=type, unit=unit, ) ``` ### property_counts ``` property_counts( event: str, property_name: str, *, from_date: str, to_date: str, type: Literal["general", "unique", "average"] = "general", unit: Literal["day", "week", "month"] = "day", values: list[str] | None = None, limit: int | None = None, ) -> PropertyCountsResult ``` Get event counts broken down by property values. | PARAMETER | DESCRIPTION | | --------------- | --------------------------------------------------------------------------------------------- | | `event` | Event name. **TYPE:** `str` | | `property_name` | Property to break down by. **TYPE:** `str` | | `from_date` | Start date (YYYY-MM-DD). **TYPE:** `str` | | `to_date` | End date (YYYY-MM-DD). **TYPE:** `str` | | `type` | Counting method. **TYPE:** `Literal['general', 'unique', 'average']` **DEFAULT:** `'general'` | | `unit` | Time unit. **TYPE:** `Literal['day', 'week', 'month']` **DEFAULT:** `'day'` | | `values` | Optional list of property values to include. **TYPE:** \`list[str] | | `limit` | Maximum number of property values. **TYPE:** \`int | | RETURNS | DESCRIPTION | | ---------------------- | --------------------------------------------------------- | | `PropertyCountsResult` | PropertyCountsResult with time-series per property value. | | RAISES | DESCRIPTION | | ------------- | --------------------------------- | | `ConfigError` | If API credentials not available. | Source code in `src/mixpanel_headless/workspace.py` ``` def property_counts( self, event: str, property_name: str, *, from_date: str, to_date: str, type: Literal["general", "unique", "average"] = "general", unit: Literal["day", "week", "month"] = "day", values: list[str] | None = None, limit: int | None = None, ) -> PropertyCountsResult: """Get event counts broken down by property values. Args: event: Event name. property_name: Property to break down by. from_date: Start date (YYYY-MM-DD). to_date: End date (YYYY-MM-DD). type: Counting method. unit: Time unit. values: Optional list of property values to include. limit: Maximum number of property values. Returns: PropertyCountsResult with time-series per property value. Raises: ConfigError: If API credentials not available. """ return self._live_query_service.property_counts( event=event, property_name=property_name, from_date=from_date, to_date=to_date, type=type, unit=unit, values=values, limit=limit, ) ``` ### activity_feed ``` activity_feed( distinct_ids: list[str], *, from_date: str | None = None, to_date: str | None = None, ) -> ActivityFeedResult ``` Get activity feed for specific users. | PARAMETER | DESCRIPTION | | -------------- | ----------------------------------------------- | | `distinct_ids` | List of user identifiers. **TYPE:** `list[str]` | | `from_date` | Optional start date filter. **TYPE:** \`str | | `to_date` | Optional end date filter. **TYPE:** \`str | | RETURNS | DESCRIPTION | | -------------------- | ------------------------------------ | | `ActivityFeedResult` | ActivityFeedResult with user events. | | RAISES | DESCRIPTION | | ------------- | --------------------------------- | | `ConfigError` | If API credentials not available. | Source code in `src/mixpanel_headless/workspace.py` ``` def activity_feed( self, distinct_ids: list[str], *, from_date: str | None = None, to_date: str | None = None, ) -> ActivityFeedResult: """Get activity feed for specific users. Args: distinct_ids: List of user identifiers. from_date: Optional start date filter. to_date: Optional end date filter. Returns: ActivityFeedResult with user events. Raises: ConfigError: If API credentials not available. """ return self._live_query_service.activity_feed( distinct_ids=distinct_ids, from_date=from_date, to_date=to_date, ) ``` ### query_saved_report ``` query_saved_report( bookmark_id: int, *, bookmark_type: Literal[ "insights", "funnels", "retention", "flows" ] = "insights", from_date: str | None = None, to_date: str | None = None, ) -> SavedReportResult ``` Query a saved report by bookmark type. Routes to the appropriate Mixpanel API endpoint based on bookmark_type and returns the normalized result. | PARAMETER | DESCRIPTION | | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `bookmark_id` | ID of saved report (from list_bookmarks or Mixpanel URL). **TYPE:** `int` | | `bookmark_type` | Type of bookmark to query. Determines which API endpoint is called. Defaults to 'insights'. **TYPE:** `Literal['insights', 'funnels', 'retention', 'flows']` **DEFAULT:** `'insights'` | | `from_date` | Start date (YYYY-MM-DD). Required for funnels, optional otherwise. **TYPE:** \`str | | `to_date` | End date (YYYY-MM-DD). Required for funnels, optional otherwise. **TYPE:** \`str | | RETURNS | DESCRIPTION | | ------------------- | ------------------------------------------------------------ | | `SavedReportResult` | SavedReportResult with report data and report_type property. | | RAISES | DESCRIPTION | | ------------- | ---------------------------------------------- | | `ConfigError` | If API credentials not available. | | `QueryError` | If bookmark_id is invalid or report not found. | Source code in `src/mixpanel_headless/workspace.py` ``` def query_saved_report( self, bookmark_id: int, *, bookmark_type: Literal[ "insights", "funnels", "retention", "flows" ] = "insights", from_date: str | None = None, to_date: str | None = None, ) -> SavedReportResult: """Query a saved report by bookmark type. Routes to the appropriate Mixpanel API endpoint based on bookmark_type and returns the normalized result. Args: bookmark_id: ID of saved report (from list_bookmarks or Mixpanel URL). bookmark_type: Type of bookmark to query. Determines which API endpoint is called. Defaults to 'insights'. from_date: Start date (YYYY-MM-DD). Required for funnels, optional otherwise. to_date: End date (YYYY-MM-DD). Required for funnels, optional otherwise. Returns: SavedReportResult with report data and report_type property. Raises: ConfigError: If API credentials not available. QueryError: If bookmark_id is invalid or report not found. """ return self._live_query_service.query_saved_report( bookmark_id=bookmark_id, bookmark_type=bookmark_type, from_date=from_date, to_date=to_date, ) ``` ### query_saved_flows ``` query_saved_flows(bookmark_id: int) -> FlowsResult ``` Query a saved Flows report. Executes a saved Flows report by its bookmark ID, returning step data, breakdowns, and conversion rates. | PARAMETER | DESCRIPTION | | ------------- | ------------------------------------------------------------------------------- | | `bookmark_id` | ID of saved flows report (from list_bookmarks or Mixpanel URL). **TYPE:** `int` | | RETURNS | DESCRIPTION | | ------------- | -------------------------------------------------------- | | `FlowsResult` | FlowsResult with steps, breakdowns, and conversion rate. | | RAISES | DESCRIPTION | | ------------- | ---------------------------------------------- | | `ConfigError` | If API credentials not available. | | `QueryError` | If bookmark_id is invalid or report not found. | Source code in `src/mixpanel_headless/workspace.py` ``` def query_saved_flows(self, bookmark_id: int) -> FlowsResult: """Query a saved Flows report. Executes a saved Flows report by its bookmark ID, returning step data, breakdowns, and conversion rates. Args: bookmark_id: ID of saved flows report (from list_bookmarks or Mixpanel URL). Returns: FlowsResult with steps, breakdowns, and conversion rate. Raises: ConfigError: If API credentials not available. QueryError: If bookmark_id is invalid or report not found. """ return self._live_query_service.query_saved_flows(bookmark_id=bookmark_id) ``` ### frequency ``` frequency( *, from_date: str, to_date: str, unit: Literal["day", "week", "month"] = "day", addiction_unit: Literal["hour", "day"] = "hour", event: str | None = None, where: str | None = None, ) -> FrequencyResult ``` Analyze event frequency distribution. | PARAMETER | DESCRIPTION | | ---------------- | ----------------------------------------------------------------------------------- | | `from_date` | Start date (YYYY-MM-DD). **TYPE:** `str` | | `to_date` | End date (YYYY-MM-DD). **TYPE:** `str` | | `unit` | Overall time unit. **TYPE:** `Literal['day', 'week', 'month']` **DEFAULT:** `'day'` | | `addiction_unit` | Measurement granularity. **TYPE:** `Literal['hour', 'day']` **DEFAULT:** `'hour'` | | `event` | Optional event filter. **TYPE:** \`str | | `where` | Optional WHERE clause. **TYPE:** \`str | | RETURNS | DESCRIPTION | | ----------------- | -------------------------------------------- | | `FrequencyResult` | FrequencyResult with frequency distribution. | | RAISES | DESCRIPTION | | ------------- | --------------------------------- | | `ConfigError` | If API credentials not available. | Source code in `src/mixpanel_headless/workspace.py` ``` def frequency( self, *, from_date: str, to_date: str, unit: Literal["day", "week", "month"] = "day", addiction_unit: Literal["hour", "day"] = "hour", event: str | None = None, where: str | None = None, ) -> FrequencyResult: """Analyze event frequency distribution. Args: from_date: Start date (YYYY-MM-DD). to_date: End date (YYYY-MM-DD). unit: Overall time unit. addiction_unit: Measurement granularity. event: Optional event filter. where: Optional WHERE clause. Returns: FrequencyResult with frequency distribution. Raises: ConfigError: If API credentials not available. """ return self._live_query_service.frequency( from_date=from_date, to_date=to_date, unit=unit, addiction_unit=addiction_unit, event=event, where=where, ) ``` ### segmentation_numeric ``` segmentation_numeric( event: str, *, from_date: str, to_date: str, on: str, unit: Literal["hour", "day"] = "day", where: str | None = None, type: Literal["general", "unique", "average"] = "general", ) -> NumericBucketResult ``` Bucket events by numeric property ranges. | PARAMETER | DESCRIPTION | | ----------- | --------------------------------------------------------------------------------------------- | | `event` | Event name. **TYPE:** `str` | | `from_date` | Start date. **TYPE:** `str` | | `to_date` | End date. **TYPE:** `str` | | `on` | Numeric property expression. **TYPE:** `str` | | `unit` | Time unit. **TYPE:** `Literal['hour', 'day']` **DEFAULT:** `'day'` | | `where` | Optional filter. **TYPE:** \`str | | `type` | Counting method. **TYPE:** `Literal['general', 'unique', 'average']` **DEFAULT:** `'general'` | | RETURNS | DESCRIPTION | | --------------------- | --------------------------------------- | | `NumericBucketResult` | NumericBucketResult with bucketed data. | | RAISES | DESCRIPTION | | ------------- | --------------------------------- | | `ConfigError` | If API credentials not available. | Source code in `src/mixpanel_headless/workspace.py` ``` def segmentation_numeric( self, event: str, *, from_date: str, to_date: str, on: str, unit: Literal["hour", "day"] = "day", where: str | None = None, type: Literal["general", "unique", "average"] = "general", ) -> NumericBucketResult: """Bucket events by numeric property ranges. Args: event: Event name. from_date: Start date. to_date: End date. on: Numeric property expression. unit: Time unit. where: Optional filter. type: Counting method. Returns: NumericBucketResult with bucketed data. Raises: ConfigError: If API credentials not available. """ return self._live_query_service.segmentation_numeric( event=event, from_date=from_date, to_date=to_date, on=on, unit=unit, where=where, type=type, ) ``` ### segmentation_sum ``` segmentation_sum( event: str, *, from_date: str, to_date: str, on: str, unit: Literal["hour", "day"] = "day", where: str | None = None, ) -> NumericSumResult ``` Calculate sum of numeric property over time. | PARAMETER | DESCRIPTION | | ----------- | ------------------------------------------------------------------ | | `event` | Event name. **TYPE:** `str` | | `from_date` | Start date. **TYPE:** `str` | | `to_date` | End date. **TYPE:** `str` | | `on` | Numeric property expression. **TYPE:** `str` | | `unit` | Time unit. **TYPE:** `Literal['hour', 'day']` **DEFAULT:** `'day'` | | `where` | Optional filter. **TYPE:** \`str | | RETURNS | DESCRIPTION | | ------------------ | -------------------------------------------- | | `NumericSumResult` | NumericSumResult with sum values per period. | | RAISES | DESCRIPTION | | ------------- | --------------------------------- | | `ConfigError` | If API credentials not available. | Source code in `src/mixpanel_headless/workspace.py` ``` def segmentation_sum( self, event: str, *, from_date: str, to_date: str, on: str, unit: Literal["hour", "day"] = "day", where: str | None = None, ) -> NumericSumResult: """Calculate sum of numeric property over time. Args: event: Event name. from_date: Start date. to_date: End date. on: Numeric property expression. unit: Time unit. where: Optional filter. Returns: NumericSumResult with sum values per period. Raises: ConfigError: If API credentials not available. """ return self._live_query_service.segmentation_sum( event=event, from_date=from_date, to_date=to_date, on=on, unit=unit, where=where, ) ``` ### segmentation_average ``` segmentation_average( event: str, *, from_date: str, to_date: str, on: str, unit: Literal["hour", "day"] = "day", where: str | None = None, ) -> NumericAverageResult ``` Calculate average of numeric property over time. | PARAMETER | DESCRIPTION | | ----------- | ------------------------------------------------------------------ | | `event` | Event name. **TYPE:** `str` | | `from_date` | Start date. **TYPE:** `str` | | `to_date` | End date. **TYPE:** `str` | | `on` | Numeric property expression. **TYPE:** `str` | | `unit` | Time unit. **TYPE:** `Literal['hour', 'day']` **DEFAULT:** `'day'` | | `where` | Optional filter. **TYPE:** \`str | | RETURNS | DESCRIPTION | | ---------------------- | ---------------------------------------------------- | | `NumericAverageResult` | NumericAverageResult with average values per period. | | RAISES | DESCRIPTION | | ------------- | --------------------------------- | | `ConfigError` | If API credentials not available. | Source code in `src/mixpanel_headless/workspace.py` ``` def segmentation_average( self, event: str, *, from_date: str, to_date: str, on: str, unit: Literal["hour", "day"] = "day", where: str | None = None, ) -> NumericAverageResult: """Calculate average of numeric property over time. Args: event: Event name. from_date: Start date. to_date: End date. on: Numeric property expression. unit: Time unit. where: Optional filter. Returns: NumericAverageResult with average values per period. Raises: ConfigError: If API credentials not available. """ return self._live_query_service.segmentation_average( event=event, from_date=from_date, to_date=to_date, on=on, unit=unit, where=where, ) ``` ### property_distribution ``` property_distribution( event: str, property: str, *, from_date: str, to_date: str, limit: int = 20 ) -> PropertyDistributionResult ``` Get distribution of values for a property. Uses JQL to count occurrences of each property value, returning counts and percentages sorted by frequency. | PARAMETER | DESCRIPTION | | ----------- | ---------------------------------------------------------------------------------- | | `event` | Event name to analyze. **TYPE:** `str` | | `property` | Property name to get distribution for. **TYPE:** `str` | | `from_date` | Start date (YYYY-MM-DD). **TYPE:** `str` | | `to_date` | End date (YYYY-MM-DD). **TYPE:** `str` | | `limit` | Maximum number of values to return. Default: 20. **TYPE:** `int` **DEFAULT:** `20` | | RETURNS | DESCRIPTION | | ---------------------------- | ------------------------------------------------------------- | | `PropertyDistributionResult` | PropertyDistributionResult with value counts and percentages. | | RAISES | DESCRIPTION | | ------------- | --------------------------------- | | `ConfigError` | If API credentials not available. | | `QueryError` | Script execution error. | Example ``` result = ws.property_distribution( event="Purchase", property="country", from_date="2024-01-01", to_date="2024-01-31", ) for v in result.values: print(f"{v.value}: {v.count} ({v.percentage:.1f}%)") ``` Source code in `src/mixpanel_headless/workspace.py` ```` def property_distribution( self, event: str, property: str, *, from_date: str, to_date: str, limit: int = 20, ) -> PropertyDistributionResult: """Get distribution of values for a property. Uses JQL to count occurrences of each property value, returning counts and percentages sorted by frequency. Args: event: Event name to analyze. property: Property name to get distribution for. from_date: Start date (YYYY-MM-DD). to_date: End date (YYYY-MM-DD). limit: Maximum number of values to return. Default: 20. Returns: PropertyDistributionResult with value counts and percentages. Raises: ConfigError: If API credentials not available. QueryError: Script execution error. Example: ```python result = ws.property_distribution( event="Purchase", property="country", from_date="2024-01-01", to_date="2024-01-31", ) for v in result.values: print(f"{v.value}: {v.count} ({v.percentage:.1f}%)") ``` """ return self._live_query_service.property_distribution( event=event, property=property, from_date=from_date, to_date=to_date, limit=limit, ) ```` ### numeric_summary ``` numeric_summary( event: str, property: str, *, from_date: str, to_date: str, percentiles: list[int] | None = None, ) -> NumericPropertySummaryResult ``` Get statistical summary for a numeric property. Uses JQL to compute count, min, max, avg, stddev, and percentiles for a numeric property. | PARAMETER | DESCRIPTION | | ------------- | -------------------------------------------------------------------------------- | | `event` | Event name to analyze. **TYPE:** `str` | | `property` | Numeric property name. **TYPE:** `str` | | `from_date` | Start date (YYYY-MM-DD). **TYPE:** `str` | | `to_date` | End date (YYYY-MM-DD). **TYPE:** `str` | | `percentiles` | Percentiles to compute. Default: [25, 50, 75, 90, 95, 99]. **TYPE:** \`list[int] | | RETURNS | DESCRIPTION | | ------------------------------ | --------------------------------------------- | | `NumericPropertySummaryResult` | NumericPropertySummaryResult with statistics. | | RAISES | DESCRIPTION | | ------------- | ----------------------------------------------- | | `ConfigError` | If API credentials not available. | | `QueryError` | Script execution error or non-numeric property. | Example ``` result = ws.numeric_summary( event="Purchase", property="amount", from_date="2024-01-01", to_date="2024-01-31", ) print(f"Avg: {result.avg}, Median: {result.percentiles[50]}") ``` Source code in `src/mixpanel_headless/workspace.py` ```` def numeric_summary( self, event: str, property: str, *, from_date: str, to_date: str, percentiles: list[int] | None = None, ) -> NumericPropertySummaryResult: """Get statistical summary for a numeric property. Uses JQL to compute count, min, max, avg, stddev, and percentiles for a numeric property. Args: event: Event name to analyze. property: Numeric property name. from_date: Start date (YYYY-MM-DD). to_date: End date (YYYY-MM-DD). percentiles: Percentiles to compute. Default: [25, 50, 75, 90, 95, 99]. Returns: NumericPropertySummaryResult with statistics. Raises: ConfigError: If API credentials not available. QueryError: Script execution error or non-numeric property. Example: ```python result = ws.numeric_summary( event="Purchase", property="amount", from_date="2024-01-01", to_date="2024-01-31", ) print(f"Avg: {result.avg}, Median: {result.percentiles[50]}") ``` """ return self._live_query_service.numeric_summary( event=event, property=property, from_date=from_date, to_date=to_date, percentiles=percentiles, ) ```` ### daily_counts ``` daily_counts( *, from_date: str, to_date: str, events: list[str] | None = None ) -> DailyCountsResult ``` Get daily event counts. Uses JQL to count events by day, optionally filtered to specific events. | PARAMETER | DESCRIPTION | | ----------- | -------------------------------------------------------------------------- | | `from_date` | Start date (YYYY-MM-DD). **TYPE:** `str` | | `to_date` | End date (YYYY-MM-DD). **TYPE:** `str` | | `events` | Optional list of events to count. None = all events. **TYPE:** \`list[str] | | RETURNS | DESCRIPTION | | ------------------- | ----------------------------------------------- | | `DailyCountsResult` | DailyCountsResult with date/event/count tuples. | | RAISES | DESCRIPTION | | ------------- | --------------------------------- | | `ConfigError` | If API credentials not available. | | `QueryError` | Script execution error. | Example ``` result = ws.daily_counts( from_date="2024-01-01", to_date="2024-01-07", events=["Purchase", "Signup"], ) for c in result.counts: print(f"{c.date} {c.event}: {c.count}") ``` Source code in `src/mixpanel_headless/workspace.py` ```` def daily_counts( self, *, from_date: str, to_date: str, events: list[str] | None = None, ) -> DailyCountsResult: """Get daily event counts. Uses JQL to count events by day, optionally filtered to specific events. Args: from_date: Start date (YYYY-MM-DD). to_date: End date (YYYY-MM-DD). events: Optional list of events to count. None = all events. Returns: DailyCountsResult with date/event/count tuples. Raises: ConfigError: If API credentials not available. QueryError: Script execution error. Example: ```python result = ws.daily_counts( from_date="2024-01-01", to_date="2024-01-07", events=["Purchase", "Signup"], ) for c in result.counts: print(f"{c.date} {c.event}: {c.count}") ``` """ return self._live_query_service.daily_counts( from_date=from_date, to_date=to_date, events=events, ) ```` ### engagement_distribution ``` engagement_distribution( *, from_date: str, to_date: str, events: list[str] | None = None, buckets: list[int] | None = None, ) -> EngagementDistributionResult ``` Get user engagement distribution. Uses JQL to bucket users by their event count, showing how many users performed N events. | PARAMETER | DESCRIPTION | | ----------- | ----------------------------------------------------------------------------- | | `from_date` | Start date (YYYY-MM-DD). **TYPE:** `str` | | `to_date` | End date (YYYY-MM-DD). **TYPE:** `str` | | `events` | Optional list of events to count. None = all events. **TYPE:** \`list[str] | | `buckets` | Bucket boundaries. Default: [1, 2, 5, 10, 25, 50, 100]. **TYPE:** \`list[int] | | RETURNS | DESCRIPTION | | ------------------------------ | --------------------------------------------------------- | | `EngagementDistributionResult` | EngagementDistributionResult with user counts per bucket. | | RAISES | DESCRIPTION | | ------------- | --------------------------------- | | `ConfigError` | If API credentials not available. | | `QueryError` | Script execution error. | Example ``` result = ws.engagement_distribution( from_date="2024-01-01", to_date="2024-01-31", ) for b in result.buckets: print(f"{b.bucket_label}: {b.user_count} ({b.percentage:.1f}%)") ``` Source code in `src/mixpanel_headless/workspace.py` ```` def engagement_distribution( self, *, from_date: str, to_date: str, events: list[str] | None = None, buckets: list[int] | None = None, ) -> EngagementDistributionResult: """Get user engagement distribution. Uses JQL to bucket users by their event count, showing how many users performed N events. Args: from_date: Start date (YYYY-MM-DD). to_date: End date (YYYY-MM-DD). events: Optional list of events to count. None = all events. buckets: Bucket boundaries. Default: [1, 2, 5, 10, 25, 50, 100]. Returns: EngagementDistributionResult with user counts per bucket. Raises: ConfigError: If API credentials not available. QueryError: Script execution error. Example: ```python result = ws.engagement_distribution( from_date="2024-01-01", to_date="2024-01-31", ) for b in result.buckets: print(f"{b.bucket_label}: {b.user_count} ({b.percentage:.1f}%)") ``` """ return self._live_query_service.engagement_distribution( from_date=from_date, to_date=to_date, events=events, buckets=buckets, ) ```` ### property_coverage ``` property_coverage( event: str, properties: list[str], *, from_date: str, to_date: str ) -> PropertyCoverageResult ``` Get property coverage statistics. Uses JQL to count how often each property is defined (non-null) vs undefined for the specified event. | PARAMETER | DESCRIPTION | | ------------ | ------------------------------------------------------ | | `event` | Event name to analyze. **TYPE:** `str` | | `properties` | List of property names to check. **TYPE:** `list[str]` | | `from_date` | Start date (YYYY-MM-DD). **TYPE:** `str` | | `to_date` | End date (YYYY-MM-DD). **TYPE:** `str` | | RETURNS | DESCRIPTION | | ------------------------ | ------------------------------------------------------------- | | `PropertyCoverageResult` | PropertyCoverageResult with coverage statistics per property. | | RAISES | DESCRIPTION | | ------------- | --------------------------------- | | `ConfigError` | If API credentials not available. | | `QueryError` | Script execution error. | Example ``` result = ws.property_coverage( event="Purchase", properties=["coupon_code", "referrer"], from_date="2024-01-01", to_date="2024-01-31", ) for c in result.coverage: print(f"{c.property}: {c.coverage_percentage:.1f}% defined") ``` Source code in `src/mixpanel_headless/workspace.py` ```` def property_coverage( self, event: str, properties: list[str], *, from_date: str, to_date: str, ) -> PropertyCoverageResult: """Get property coverage statistics. Uses JQL to count how often each property is defined (non-null) vs undefined for the specified event. Args: event: Event name to analyze. properties: List of property names to check. from_date: Start date (YYYY-MM-DD). to_date: End date (YYYY-MM-DD). Returns: PropertyCoverageResult with coverage statistics per property. Raises: ConfigError: If API credentials not available. QueryError: Script execution error. Example: ```python result = ws.property_coverage( event="Purchase", properties=["coupon_code", "referrer"], from_date="2024-01-01", to_date="2024-01-31", ) for c in result.coverage: print(f"{c.property}: {c.coverage_percentage:.1f}% defined") ``` """ return self._live_query_service.property_coverage( event=event, properties=properties, from_date=from_date, to_date=to_date, ) ```` ### list_dashboards ``` list_dashboards(*, ids: list[int] | None = None) -> list[Dashboard] ``` List dashboards for the current project/workspace. Retrieves all dashboards visible to the authenticated user, optionally filtered by specific IDs. | PARAMETER | DESCRIPTION | | --------- | ------------------------------------------------------------------ | | `ids` | Optional list of dashboard IDs to filter by. **TYPE:** \`list[int] | | RETURNS | DESCRIPTION | | ----------------- | -------------------------- | | `list[Dashboard]` | List of Dashboard objects. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | API error (400, 404). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() dashboards = ws.list_dashboards() for d in dashboards: print(f"{d.title} (id={d.id})") ``` Source code in `src/mixpanel_headless/workspace.py` ```` def list_dashboards(self, *, ids: list[int] | None = None) -> list[Dashboard]: """List dashboards for the current project/workspace. Retrieves all dashboards visible to the authenticated user, optionally filtered by specific IDs. Args: ids: Optional list of dashboard IDs to filter by. Returns: List of ``Dashboard`` objects. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: API error (400, 404). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() dashboards = ws.list_dashboards() for d in dashboards: print(f"{d.title} (id={d.id})") ``` """ client = self._require_api_client() raw = client.list_dashboards(ids=ids) return [Dashboard.model_validate(d) for d in raw] ```` ### create_dashboard ``` create_dashboard(params: CreateDashboardParams) -> Dashboard ``` Create a new dashboard. | PARAMETER | DESCRIPTION | | --------- | ---------------------------------------------------------------- | | `params` | Dashboard creation parameters. **TYPE:** `CreateDashboardParams` | | RETURNS | DESCRIPTION | | ----------- | ---------------------------- | | `Dashboard` | The newly created Dashboard. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Invalid parameters (400, 422). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() dashboard = ws.create_dashboard( CreateDashboardParams(title="Q1 Metrics") ) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def create_dashboard(self, params: CreateDashboardParams) -> Dashboard: """Create a new dashboard. Args: params: Dashboard creation parameters. Returns: The newly created ``Dashboard``. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Invalid parameters (400, 422). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() dashboard = ws.create_dashboard( CreateDashboardParams(title="Q1 Metrics") ) ``` """ client = self._require_api_client() raw = client.create_dashboard(params.model_dump(exclude_none=True)) if raw is None: raise MixpanelHeadlessError( "API returned empty response for create_dashboard", ) return Dashboard.model_validate(raw) ```` ### get_dashboard ``` get_dashboard(dashboard_id: int) -> Dashboard ``` Get a single dashboard by ID. | PARAMETER | DESCRIPTION | | -------------- | ------------------------------------- | | `dashboard_id` | Dashboard identifier. **TYPE:** `int` | | RETURNS | DESCRIPTION | | ----------- | --------------------- | | `Dashboard` | The Dashboard object. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Dashboard not found (404). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() dashboard = ws.get_dashboard(12345) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def get_dashboard(self, dashboard_id: int) -> Dashboard: """Get a single dashboard by ID. Args: dashboard_id: Dashboard identifier. Returns: The ``Dashboard`` object. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Dashboard not found (404). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() dashboard = ws.get_dashboard(12345) ``` """ client = self._require_api_client() raw = client.get_dashboard(dashboard_id) if raw is None: raise MixpanelHeadlessError( "API returned empty response for get_dashboard", ) return Dashboard.model_validate(raw) ```` ### update_dashboard ``` update_dashboard(dashboard_id: int, params: UpdateDashboardParams) -> Dashboard ``` Update an existing dashboard. | PARAMETER | DESCRIPTION | | -------------- | --------------------------------------------------- | | `dashboard_id` | Dashboard identifier. **TYPE:** `int` | | `params` | Fields to update. **TYPE:** `UpdateDashboardParams` | | RETURNS | DESCRIPTION | | ----------- | ---------------------- | | `Dashboard` | The updated Dashboard. | | RAISES | DESCRIPTION | | --------------------- | ------------------------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Dashboard not found or invalid params (400, 404). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() updated = ws.update_dashboard( 12345, UpdateDashboardParams(title="New Title") ) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def update_dashboard( self, dashboard_id: int, params: UpdateDashboardParams ) -> Dashboard: """Update an existing dashboard. Args: dashboard_id: Dashboard identifier. params: Fields to update. Returns: The updated ``Dashboard``. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Dashboard not found or invalid params (400, 404). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() updated = ws.update_dashboard( 12345, UpdateDashboardParams(title="New Title") ) ``` """ client = self._require_api_client() raw = client.update_dashboard( dashboard_id, params.model_dump(exclude_none=True) ) if raw is None: raise MixpanelHeadlessError( "API returned empty response for update_dashboard", ) return Dashboard.model_validate(raw) ```` ### delete_dashboard ``` delete_dashboard(dashboard_id: int) -> None ``` Delete a dashboard. | PARAMETER | DESCRIPTION | | -------------- | ------------------------------------- | | `dashboard_id` | Dashboard identifier. **TYPE:** `int` | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Dashboard not found (404). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() ws.delete_dashboard(12345) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def delete_dashboard(self, dashboard_id: int) -> None: """Delete a dashboard. Args: dashboard_id: Dashboard identifier. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Dashboard not found (404). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() ws.delete_dashboard(12345) ``` """ client = self._require_api_client() client.delete_dashboard(dashboard_id) ```` ### bulk_delete_dashboards ``` bulk_delete_dashboards(ids: list[int]) -> None ``` Delete multiple dashboards. | PARAMETER | DESCRIPTION | | --------- | ------------------------------------------------------ | | `ids` | List of dashboard IDs to delete. **TYPE:** `list[int]` | | RAISES | DESCRIPTION | | --------------------- | ------------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | One or more IDs not found (400, 404). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() ws.bulk_delete_dashboards([1, 2, 3]) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def bulk_delete_dashboards(self, ids: list[int]) -> None: """Delete multiple dashboards. Args: ids: List of dashboard IDs to delete. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: One or more IDs not found (400, 404). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() ws.bulk_delete_dashboards([1, 2, 3]) ``` """ client = self._require_api_client() client.bulk_delete_dashboards(ids) ```` ### favorite_dashboard ``` favorite_dashboard(dashboard_id: int) -> None ``` Favorite a dashboard. | PARAMETER | DESCRIPTION | | -------------- | ------------------------------------- | | `dashboard_id` | Dashboard identifier. **TYPE:** `int` | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Dashboard not found (404). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() ws.favorite_dashboard(12345) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def favorite_dashboard(self, dashboard_id: int) -> None: """Favorite a dashboard. Args: dashboard_id: Dashboard identifier. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Dashboard not found (404). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() ws.favorite_dashboard(12345) ``` """ client = self._require_api_client() client.favorite_dashboard(dashboard_id) ```` ### unfavorite_dashboard ``` unfavorite_dashboard(dashboard_id: int) -> None ``` Unfavorite a dashboard. | PARAMETER | DESCRIPTION | | -------------- | ------------------------------------- | | `dashboard_id` | Dashboard identifier. **TYPE:** `int` | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Dashboard not found (404). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() ws.unfavorite_dashboard(12345) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def unfavorite_dashboard(self, dashboard_id: int) -> None: """Unfavorite a dashboard. Args: dashboard_id: Dashboard identifier. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Dashboard not found (404). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() ws.unfavorite_dashboard(12345) ``` """ client = self._require_api_client() client.unfavorite_dashboard(dashboard_id) ```` ### pin_dashboard ``` pin_dashboard(dashboard_id: int) -> None ``` Pin a dashboard. | PARAMETER | DESCRIPTION | | -------------- | ------------------------------------- | | `dashboard_id` | Dashboard identifier. **TYPE:** `int` | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Dashboard not found (404). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() ws.pin_dashboard(12345) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def pin_dashboard(self, dashboard_id: int) -> None: """Pin a dashboard. Args: dashboard_id: Dashboard identifier. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Dashboard not found (404). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() ws.pin_dashboard(12345) ``` """ client = self._require_api_client() client.pin_dashboard(dashboard_id) ```` ### unpin_dashboard ``` unpin_dashboard(dashboard_id: int) -> None ``` Unpin a dashboard. | PARAMETER | DESCRIPTION | | -------------- | ------------------------------------- | | `dashboard_id` | Dashboard identifier. **TYPE:** `int` | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Dashboard not found (404). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() ws.unpin_dashboard(12345) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def unpin_dashboard(self, dashboard_id: int) -> None: """Unpin a dashboard. Args: dashboard_id: Dashboard identifier. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Dashboard not found (404). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() ws.unpin_dashboard(12345) ``` """ client = self._require_api_client() client.unpin_dashboard(dashboard_id) ```` ### add_report_to_dashboard ``` add_report_to_dashboard(dashboard_id: int, bookmark_id: int) -> Dashboard ``` Add a report to a dashboard. Clones the specified bookmark onto the dashboard. The cloned report appears as a new card in the dashboard layout. | PARAMETER | DESCRIPTION | | -------------- | -------------------------------------------------- | | `dashboard_id` | Dashboard identifier. **TYPE:** `int` | | `bookmark_id` | Bookmark/report identifier to add. **TYPE:** `int` | | RETURNS | DESCRIPTION | | ----------- | ---------------------- | | `Dashboard` | The updated Dashboard. | | RAISES | DESCRIPTION | | ----------------------- | -------------------------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Dashboard or bookmark not found (404). | | `ServerError` | Server-side errors (5xx). | | `MixpanelHeadlessError` | If the API response is not a valid dashboard dict. | Example ``` ws = Workspace() updated = ws.add_report_to_dashboard(12345, 42) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def add_report_to_dashboard(self, dashboard_id: int, bookmark_id: int) -> Dashboard: """Add a report to a dashboard. Clones the specified bookmark onto the dashboard. The cloned report appears as a new card in the dashboard layout. Args: dashboard_id: Dashboard identifier. bookmark_id: Bookmark/report identifier to add. Returns: The updated ``Dashboard``. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Dashboard or bookmark not found (404). ServerError: Server-side errors (5xx). MixpanelHeadlessError: If the API response is not a valid dashboard dict. Example: ```python ws = Workspace() updated = ws.add_report_to_dashboard(12345, 42) ``` """ client = self._require_api_client() raw = client.add_report_to_dashboard(dashboard_id, bookmark_id) if not isinstance(raw, dict) or "id" not in raw: raise MixpanelHeadlessError( "Unexpected response from add_report_to_dashboard: " f"expected dashboard dict with 'id', got {raw!r}", ) return Dashboard.model_validate(raw) ```` ### remove_report_from_dashboard ``` remove_report_from_dashboard(dashboard_id: int, bookmark_id: int) -> Dashboard ``` Remove a report from a dashboard. | PARAMETER | DESCRIPTION | | -------------- | ----------------------------------------------------- | | `dashboard_id` | Dashboard identifier. **TYPE:** `int` | | `bookmark_id` | Bookmark/report identifier to remove. **TYPE:** `int` | | RETURNS | DESCRIPTION | | ----------- | ---------------------- | | `Dashboard` | The updated Dashboard. | | RAISES | DESCRIPTION | | --------------------- | -------------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Dashboard or bookmark not found (404). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() updated = ws.remove_report_from_dashboard(12345, 42) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def remove_report_from_dashboard( self, dashboard_id: int, bookmark_id: int ) -> Dashboard: """Remove a report from a dashboard. Args: dashboard_id: Dashboard identifier. bookmark_id: Bookmark/report identifier to remove. Returns: The updated ``Dashboard``. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Dashboard or bookmark not found (404). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() updated = ws.remove_report_from_dashboard(12345, 42) ``` """ client = self._require_api_client() raw = client.remove_report_from_dashboard(dashboard_id, bookmark_id) return Dashboard.model_validate(raw) ```` ### list_blueprint_templates ``` list_blueprint_templates( *, include_reports: bool = False ) -> list[BlueprintTemplate] ``` List available dashboard blueprint templates. | PARAMETER | DESCRIPTION | | ----------------- | ------------------------------------------------------------------------ | | `include_reports` | Whether to include report details. **TYPE:** `bool` **DEFAULT:** `False` | | RETURNS | DESCRIPTION | | ------------------------- | ---------------------------------- | | `list[BlueprintTemplate]` | List of BlueprintTemplate objects. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() templates = ws.list_blueprint_templates() ``` Source code in `src/mixpanel_headless/workspace.py` ```` def list_blueprint_templates( self, *, include_reports: bool = False ) -> list[BlueprintTemplate]: """List available dashboard blueprint templates. Args: include_reports: Whether to include report details. Returns: List of ``BlueprintTemplate`` objects. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() templates = ws.list_blueprint_templates() ``` """ client = self._require_api_client() raw = client.list_blueprint_templates(include_reports=include_reports) return [BlueprintTemplate.model_validate(t) for t in raw] ```` ### create_blueprint ``` create_blueprint(template_type: str) -> Dashboard ``` Create a dashboard from a blueprint template. | PARAMETER | DESCRIPTION | | --------------- | --------------------------------------------------- | | `template_type` | Blueprint template type identifier. **TYPE:** `str` | | RETURNS | DESCRIPTION | | ----------- | ---------------------------- | | `Dashboard` | The newly created Dashboard. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Invalid template type (400). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() dashboard = ws.create_blueprint("onboarding") ``` Source code in `src/mixpanel_headless/workspace.py` ```` def create_blueprint(self, template_type: str) -> Dashboard: """Create a dashboard from a blueprint template. Args: template_type: Blueprint template type identifier. Returns: The newly created ``Dashboard``. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Invalid template type (400). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() dashboard = ws.create_blueprint("onboarding") ``` """ client = self._require_api_client() raw = client.create_blueprint(template_type) if raw is None: raise MixpanelHeadlessError( "API returned empty response for create_blueprint", ) return Dashboard.model_validate(raw) ```` ### get_blueprint_config ``` get_blueprint_config(dashboard_id: int) -> BlueprintConfig ``` Get the blueprint configuration for a dashboard. | PARAMETER | DESCRIPTION | | -------------- | ------------------------------------- | | `dashboard_id` | Dashboard identifier. **TYPE:** `int` | | RETURNS | DESCRIPTION | | ----------------- | ---------------------------------------- | | `BlueprintConfig` | BlueprintConfig with template variables. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Dashboard not found (404). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() config = ws.get_blueprint_config(12345) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def get_blueprint_config(self, dashboard_id: int) -> BlueprintConfig: """Get the blueprint configuration for a dashboard. Args: dashboard_id: Dashboard identifier. Returns: ``BlueprintConfig`` with template variables. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Dashboard not found (404). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() config = ws.get_blueprint_config(12345) ``` """ client = self._require_api_client() raw = client.get_blueprint_config(dashboard_id) if raw is None: raise MixpanelHeadlessError( "API returned empty response for get_blueprint_config", ) return BlueprintConfig.model_validate(raw) ```` ### update_blueprint_cohorts ``` update_blueprint_cohorts(cohorts: list[dict[str, Any]]) -> None ``` Update cohorts for blueprint configuration. | PARAMETER | DESCRIPTION | | --------- | -------------------------------------------------------------------- | | `cohorts` | List of cohort configuration dicts. **TYPE:** `list[dict[str, Any]]` | | RAISES | DESCRIPTION | | --------------------- | ----------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Invalid cohort configuration (400). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() ws.update_blueprint_cohorts([{"id": 1, "name": "Test"}]) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def update_blueprint_cohorts(self, cohorts: list[dict[str, Any]]) -> None: """Update cohorts for blueprint configuration. Args: cohorts: List of cohort configuration dicts. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Invalid cohort configuration (400). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() ws.update_blueprint_cohorts([{"id": 1, "name": "Test"}]) ``` """ client = self._require_api_client() client.update_blueprint_cohorts(cohorts) ```` ### finalize_blueprint ``` finalize_blueprint(params: BlueprintFinishParams) -> Dashboard ``` Finalize a blueprint dashboard with cards. | PARAMETER | DESCRIPTION | | --------- | -------------------------------------------------------------------- | | `params` | Blueprint finalization parameters. **TYPE:** `BlueprintFinishParams` | | RETURNS | DESCRIPTION | | ----------- | ------------------------ | | `Dashboard` | The finalized Dashboard. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Invalid parameters (400). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() dashboard = ws.finalize_blueprint( BlueprintFinishParams( dashboard_id=1, cards=[BlueprintCard(card_type="report", bookmark_id=42)], ) ) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def finalize_blueprint(self, params: BlueprintFinishParams) -> Dashboard: """Finalize a blueprint dashboard with cards. Args: params: Blueprint finalization parameters. Returns: The finalized ``Dashboard``. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Invalid parameters (400). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() dashboard = ws.finalize_blueprint( BlueprintFinishParams( dashboard_id=1, cards=[BlueprintCard(card_type="report", bookmark_id=42)], ) ) ``` """ client = self._require_api_client() body = params.model_dump(exclude_none=True, by_alias=True) raw = client.finalize_blueprint(body) if raw is None: raise MixpanelHeadlessError( "API returned empty response for finalize_blueprint", ) return Dashboard.model_validate(raw) ```` ### create_rca_dashboard ``` create_rca_dashboard(params: CreateRcaDashboardParams) -> Dashboard ``` Create an RCA (Root Cause Analysis) dashboard. | PARAMETER | DESCRIPTION | | --------- | -------------------------------------------------------------- | | `params` | RCA dashboard parameters. **TYPE:** `CreateRcaDashboardParams` | | RETURNS | DESCRIPTION | | ----------- | ---------------------------- | | `Dashboard` | The newly created Dashboard. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Invalid parameters (400). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() dashboard = ws.create_rca_dashboard( CreateRcaDashboardParams( rca_source_id=42, rca_source_data=RcaSourceData(source_type="anomaly"), ) ) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def create_rca_dashboard(self, params: CreateRcaDashboardParams) -> Dashboard: """Create an RCA (Root Cause Analysis) dashboard. Args: params: RCA dashboard parameters. Returns: The newly created ``Dashboard``. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Invalid parameters (400). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() dashboard = ws.create_rca_dashboard( CreateRcaDashboardParams( rca_source_id=42, rca_source_data=RcaSourceData(source_type="anomaly"), ) ) ``` """ client = self._require_api_client() body = params.model_dump(exclude_none=True, by_alias=True) raw = client.create_rca_dashboard(body) if raw is None: raise MixpanelHeadlessError( "API returned empty response for create_rca_dashboard", ) return Dashboard.model_validate(raw) ```` ### get_bookmark_dashboard_ids ``` get_bookmark_dashboard_ids(bookmark_id: int) -> list[int] ``` Get dashboard IDs containing a bookmark/report. | PARAMETER | DESCRIPTION | | ------------- | ------------------------------------ | | `bookmark_id` | Bookmark identifier. **TYPE:** `int` | | RETURNS | DESCRIPTION | | ----------- | ---------------------- | | `list[int]` | List of dashboard IDs. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Bookmark not found (404). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() dash_ids = ws.get_bookmark_dashboard_ids(42) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def get_bookmark_dashboard_ids(self, bookmark_id: int) -> list[int]: """Get dashboard IDs containing a bookmark/report. Args: bookmark_id: Bookmark identifier. Returns: List of dashboard IDs. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Bookmark not found (404). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() dash_ids = ws.get_bookmark_dashboard_ids(42) ``` """ client = self._require_api_client() return client.get_bookmark_dashboard_ids(bookmark_id) ```` ### get_dashboard_erf ``` get_dashboard_erf(dashboard_id: int) -> dict[str, Any] ``` Get ERF data for a dashboard. | PARAMETER | DESCRIPTION | | -------------- | ------------------------------------- | | `dashboard_id` | Dashboard identifier. **TYPE:** `int` | | RETURNS | DESCRIPTION | | ---------------- | --------------------------- | | `dict[str, Any]` | Dict with ERF metrics data. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Dashboard not found (404). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() erf = ws.get_dashboard_erf(12345) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def get_dashboard_erf(self, dashboard_id: int) -> dict[str, Any]: """Get ERF data for a dashboard. Args: dashboard_id: Dashboard identifier. Returns: Dict with ERF metrics data. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Dashboard not found (404). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() erf = ws.get_dashboard_erf(12345) ``` """ client = self._require_api_client() return client.get_dashboard_erf(dashboard_id) ```` ### update_report_link ``` update_report_link( dashboard_id: int, report_link_id: int, params: UpdateReportLinkParams ) -> None ``` Update a report link on a dashboard. | PARAMETER | DESCRIPTION | | ---------------- | ----------------------------------------------------- | | `dashboard_id` | Dashboard identifier. **TYPE:** `int` | | `report_link_id` | Report link identifier. **TYPE:** `int` | | `params` | Update parameters. **TYPE:** `UpdateReportLinkParams` | | RAISES | DESCRIPTION | | --------------------- | ---------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Dashboard or link not found (404). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() ws.update_report_link( 12345, 42, UpdateReportLinkParams(link_type="embedded") ) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def update_report_link( self, dashboard_id: int, report_link_id: int, params: UpdateReportLinkParams, ) -> None: """Update a report link on a dashboard. Args: dashboard_id: Dashboard identifier. report_link_id: Report link identifier. params: Update parameters. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Dashboard or link not found (404). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() ws.update_report_link( 12345, 42, UpdateReportLinkParams(link_type="embedded") ) ``` """ client = self._require_api_client() client.update_report_link( dashboard_id, report_link_id, params.model_dump(by_alias=True, exclude_none=True), ) ```` ### update_text_card ``` update_text_card( dashboard_id: int, text_card_id: int, params: UpdateTextCardParams ) -> None ``` Update a text card on a dashboard. | PARAMETER | DESCRIPTION | | -------------- | --------------------------------------------------- | | `dashboard_id` | Dashboard identifier. **TYPE:** `int` | | `text_card_id` | Text card identifier. **TYPE:** `int` | | `params` | Update parameters. **TYPE:** `UpdateTextCardParams` | | RAISES | DESCRIPTION | | --------------------- | --------------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Dashboard or text card not found (404). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() ws.update_text_card( 12345, 99, UpdateTextCardParams(markdown="# Hello") ) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def update_text_card( self, dashboard_id: int, text_card_id: int, params: UpdateTextCardParams, ) -> None: """Update a text card on a dashboard. Args: dashboard_id: Dashboard identifier. text_card_id: Text card identifier. params: Update parameters. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Dashboard or text card not found (404). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() ws.update_text_card( 12345, 99, UpdateTextCardParams(markdown="# Hello") ) ``` """ client = self._require_api_client() client.update_text_card( dashboard_id, text_card_id, params.model_dump(exclude_none=True), ) ```` ### list_bookmarks_v2 ``` list_bookmarks_v2( *, bookmark_type: BookmarkType | None = None, ids: list[int] | None = None ) -> list[Bookmark] ``` List bookmarks/reports via the App API v2 endpoint. | PARAMETER | DESCRIPTION | | --------------- | ----------------------------------------------------------------------- | | `bookmark_type` | Optional report type filter (e.g., "funnels"). **TYPE:** \`BookmarkType | | `ids` | Optional list of bookmark IDs to filter by. **TYPE:** \`list[int] | | RETURNS | DESCRIPTION | | ---------------- | ------------------------- | | `list[Bookmark]` | List of Bookmark objects. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | API error (400, 404). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() reports = ws.list_bookmarks_v2(bookmark_type="funnels") for r in reports: print(f"{r.name} ({r.bookmark_type})") ``` Source code in `src/mixpanel_headless/workspace.py` ```` def list_bookmarks_v2( self, *, bookmark_type: BookmarkType | None = None, ids: list[int] | None = None, ) -> list[Bookmark]: """List bookmarks/reports via the App API v2 endpoint. Args: bookmark_type: Optional report type filter (e.g., ``"funnels"``). ids: Optional list of bookmark IDs to filter by. Returns: List of ``Bookmark`` objects. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: API error (400, 404). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() reports = ws.list_bookmarks_v2(bookmark_type="funnels") for r in reports: print(f"{r.name} ({r.bookmark_type})") ``` """ client = self._require_api_client() raw = client.list_bookmarks_v2(bookmark_type=bookmark_type, ids=ids) return [Bookmark.model_validate(b) for b in raw] ```` ### create_bookmark ``` create_bookmark(params: CreateBookmarkParams) -> Bookmark ``` Create a new bookmark (saved report). | PARAMETER | DESCRIPTION | | --------- | --------------------------------------------------------------------------------------------------------------- | | `params` | Bookmark creation parameters. dashboard_id is required by the Mixpanel v2 API. **TYPE:** `CreateBookmarkParams` | | RETURNS | DESCRIPTION | | ---------- | --------------------------- | | `Bookmark` | The newly created Bookmark. | | RAISES | DESCRIPTION | | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | | `MixpanelHeadlessError` | If params.dashboard_id is None (required by the Mixpanel v2 API). | | `BookmarkValidationError` | If params.params fails client-side schema validation (mirrors Mixpanel's canonical Pydantic schema; raised before the API call). | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Invalid parameters (400, 422). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() dashboard = ws.create_dashboard( CreateDashboardParams(title="My Dashboard") ) report = ws.create_bookmark(CreateBookmarkParams( name="Signup Funnel", bookmark_type="funnels", params={"events": [{"event": "Signup"}]}, dashboard_id=dashboard.id, )) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def create_bookmark(self, params: CreateBookmarkParams) -> Bookmark: """Create a new bookmark (saved report). Args: params: Bookmark creation parameters. ``dashboard_id`` is required by the Mixpanel v2 API. Returns: The newly created ``Bookmark``. Raises: MixpanelHeadlessError: If ``params.dashboard_id`` is ``None`` (required by the Mixpanel v2 API). BookmarkValidationError: If ``params.params`` fails client-side schema validation (mirrors Mixpanel's canonical Pydantic schema; raised before the API call). ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Invalid parameters (400, 422). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() dashboard = ws.create_dashboard( CreateDashboardParams(title="My Dashboard") ) report = ws.create_bookmark(CreateBookmarkParams( name="Signup Funnel", bookmark_type="funnels", params={"events": [{"event": "Signup"}]}, dashboard_id=dashboard.id, )) ``` """ if params.dashboard_id is None: raise MixpanelHeadlessError( "dashboard_id is required when creating a bookmark. " "The Mixpanel v2 API requires every bookmark to be " "associated with a dashboard. Create a dashboard first " "with create_dashboard(), then pass its ID here.", ) # Full Pydantic-schema validation against the canonical mirror # of Mixpanel's bookmark schema β€” catches malformed shapes # client-side before they're persisted with garbage that only # surfaces later at chart-render time. schema_errors = self._validate_bookmark_params_schema( params.params, params.bookmark_type ) if any(e.severity == "error" for e in schema_errors): raise BookmarkValidationError(schema_errors) for w in (e for e in schema_errors if e.severity == "warning"): logger.warning( "create_bookmark validation warning: %s [%s]", w.message, w.code, ) client = self._require_api_client() raw = client.create_bookmark( params.model_dump(by_alias=True, exclude_none=True) ) if raw is None: raise MixpanelHeadlessError( "API returned empty response for create_bookmark", ) bookmark = Bookmark.model_validate(raw) # The v2 create endpoint associates the bookmark with the # dashboard in the database, but does NOT add it to the # dashboard's visual layout β€” that requires a separate # PATCH call. self.add_report_to_dashboard(params.dashboard_id, bookmark.id) return bookmark ```` ### get_bookmark ``` get_bookmark(bookmark_id: int) -> Bookmark ``` Get a single bookmark by ID. | PARAMETER | DESCRIPTION | | ------------- | ------------------------------------ | | `bookmark_id` | Bookmark identifier. **TYPE:** `int` | | RETURNS | DESCRIPTION | | ---------- | -------------------- | | `Bookmark` | The Bookmark object. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Bookmark not found (404). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() report = ws.get_bookmark(12345) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def get_bookmark(self, bookmark_id: int) -> Bookmark: """Get a single bookmark by ID. Args: bookmark_id: Bookmark identifier. Returns: The ``Bookmark`` object. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Bookmark not found (404). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() report = ws.get_bookmark(12345) ``` """ client = self._require_api_client() raw = client.get_bookmark(bookmark_id) if raw is None: raise MixpanelHeadlessError( "API returned empty response for get_bookmark", ) return Bookmark.model_validate(raw) ```` ### update_bookmark ``` update_bookmark(bookmark_id: int, params: UpdateBookmarkParams) -> Bookmark ``` Update an existing bookmark. | PARAMETER | DESCRIPTION | | ------------- | -------------------------------------------------- | | `bookmark_id` | Bookmark identifier. **TYPE:** `int` | | `params` | Fields to update. **TYPE:** `UpdateBookmarkParams` | | RETURNS | DESCRIPTION | | ---------- | --------------------- | | `Bookmark` | The updated Bookmark. | | RAISES | DESCRIPTION | | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `BookmarkValidationError` | If params.params (when supplied) fails partial-mode client-side schema validation (mirrors Mixpanel's canonical schema for the keys that ARE present; raised before the API call). | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Bookmark not found or invalid params (400, 404). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() updated = ws.update_bookmark( 12345, UpdateBookmarkParams(name="Renamed") ) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def update_bookmark( self, bookmark_id: int, params: UpdateBookmarkParams ) -> Bookmark: """Update an existing bookmark. Args: bookmark_id: Bookmark identifier. params: Fields to update. Returns: The updated ``Bookmark``. Raises: BookmarkValidationError: If ``params.params`` (when supplied) fails partial-mode client-side schema validation (mirrors Mixpanel's canonical schema for the keys that ARE present; raised before the API call). ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Bookmark not found or invalid params (400, 404). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() updated = ws.update_bookmark( 12345, UpdateBookmarkParams(name="Renamed") ) ``` """ # Partial-aware Pydantic validation: each present top-level key # in ``params.params`` is checked against its canonical sub-model. # Missing top-level keys are intentional (partial updates) and # not flagged. Catches the same malformations the create-path # catches on the keys that ARE present (chartType typos, # missing colSortAttrs, extra segmentation field, etc.). if params.params is not None: schema_errors = self._validate_bookmark_params_schema( params.params, bookmark_type=None, partial=True ) if any(e.severity == "error" for e in schema_errors): raise BookmarkValidationError(schema_errors) for w in (e for e in schema_errors if e.severity == "warning"): logger.warning( "update_bookmark validation warning: %s [%s]", w.message, w.code, ) client = self._require_api_client() raw = client.update_bookmark(bookmark_id, params.model_dump(exclude_none=True)) if raw is None: raise MixpanelHeadlessError( "API returned empty response for update_bookmark", ) return Bookmark.model_validate(raw) ```` ### delete_bookmark ``` delete_bookmark(bookmark_id: int) -> None ``` Delete a bookmark. | PARAMETER | DESCRIPTION | | ------------- | ------------------------------------ | | `bookmark_id` | Bookmark identifier. **TYPE:** `int` | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Bookmark not found (404). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() ws.delete_bookmark(12345) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def delete_bookmark(self, bookmark_id: int) -> None: """Delete a bookmark. Args: bookmark_id: Bookmark identifier. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Bookmark not found (404). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() ws.delete_bookmark(12345) ``` """ client = self._require_api_client() client.delete_bookmark(bookmark_id) ```` ### bulk_delete_bookmarks ``` bulk_delete_bookmarks(ids: list[int]) -> None ``` Delete multiple bookmarks. | PARAMETER | DESCRIPTION | | --------- | ----------------------------------------------------- | | `ids` | List of bookmark IDs to delete. **TYPE:** `list[int]` | | RAISES | DESCRIPTION | | --------------------- | ------------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | One or more IDs not found (400, 404). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() ws.bulk_delete_bookmarks([1, 2, 3]) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def bulk_delete_bookmarks(self, ids: list[int]) -> None: """Delete multiple bookmarks. Args: ids: List of bookmark IDs to delete. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: One or more IDs not found (400, 404). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() ws.bulk_delete_bookmarks([1, 2, 3]) ``` """ client = self._require_api_client() client.bulk_delete_bookmarks(ids) ```` ### bulk_update_bookmarks ``` bulk_update_bookmarks(entries: list[BulkUpdateBookmarkEntry]) -> None ``` Update multiple bookmarks. | PARAMETER | DESCRIPTION | | --------- | -------------------------------------------------------------------------- | | `entries` | List of bookmark update entries. **TYPE:** `list[BulkUpdateBookmarkEntry]` | | RAISES | DESCRIPTION | | --------------------- | -------------------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Invalid entries or IDs not found (400, 404). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() ws.bulk_update_bookmarks([ BulkUpdateBookmarkEntry(id=1, name="Renamed"), ]) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def bulk_update_bookmarks(self, entries: list[BulkUpdateBookmarkEntry]) -> None: """Update multiple bookmarks. Args: entries: List of bookmark update entries. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Invalid entries or IDs not found (400, 404). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() ws.bulk_update_bookmarks([ BulkUpdateBookmarkEntry(id=1, name="Renamed"), ]) ``` """ client = self._require_api_client() client.bulk_update_bookmarks([e.model_dump(exclude_none=True) for e in entries]) ```` ### bookmark_linked_dashboard_ids ``` bookmark_linked_dashboard_ids(bookmark_id: int) -> list[int] ``` Get dashboard IDs linked to a bookmark. | PARAMETER | DESCRIPTION | | ------------- | ------------------------------------ | | `bookmark_id` | Bookmark identifier. **TYPE:** `int` | | RETURNS | DESCRIPTION | | ----------- | ---------------------- | | `list[int]` | List of dashboard IDs. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Bookmark not found (404). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() dash_ids = ws.bookmark_linked_dashboard_ids(42) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def bookmark_linked_dashboard_ids(self, bookmark_id: int) -> list[int]: """Get dashboard IDs linked to a bookmark. Args: bookmark_id: Bookmark identifier. Returns: List of dashboard IDs. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Bookmark not found (404). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() dash_ids = ws.bookmark_linked_dashboard_ids(42) ``` """ client = self._require_api_client() return client.bookmark_linked_dashboard_ids(bookmark_id) ```` ### get_bookmark_history ``` get_bookmark_history( bookmark_id: int, *, cursor: str | None = None, page_size: int | None = None ) -> BookmarkHistoryResponse ``` Get the change history for a bookmark. | PARAMETER | DESCRIPTION | | ------------- | ----------------------------------------- | | `bookmark_id` | Bookmark identifier. **TYPE:** `int` | | `cursor` | Opaque pagination cursor. **TYPE:** \`str | | `page_size` | Maximum entries per page. **TYPE:** \`int | | RETURNS | DESCRIPTION | | ------------------------- | ---------------------------------------------------- | | `BookmarkHistoryResponse` | BookmarkHistoryResponse with results and pagination. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Bookmark not found (404). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() history = ws.get_bookmark_history(12345, page_size=10) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def get_bookmark_history( self, bookmark_id: int, *, cursor: str | None = None, page_size: int | None = None, ) -> BookmarkHistoryResponse: """Get the change history for a bookmark. Args: bookmark_id: Bookmark identifier. cursor: Opaque pagination cursor. page_size: Maximum entries per page. Returns: ``BookmarkHistoryResponse`` with results and pagination. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Bookmark not found (404). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() history = ws.get_bookmark_history(12345, page_size=10) ``` """ client = self._require_api_client() raw = client.get_bookmark_history( bookmark_id, cursor=cursor, page_size=page_size ) return BookmarkHistoryResponse.model_validate(raw) ```` ### list_cohorts_full ``` list_cohorts_full( *, data_group_id: str | None = None, ids: list[int] | None = None ) -> list[Cohort] ``` List cohorts via the App API (full detail). Unlike `cohorts()` which uses the discovery endpoint, this method uses the App API and returns full `Cohort` objects with all metadata. | PARAMETER | DESCRIPTION | | --------------- | --------------------------------------------------------------- | | `data_group_id` | Optional data group filter. **TYPE:** \`str | | `ids` | Optional list of cohort IDs to filter by. **TYPE:** \`list[int] | | RETURNS | DESCRIPTION | | -------------- | ----------------------- | | `list[Cohort]` | List of Cohort objects. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | API error (400, 404). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() cohorts = ws.list_cohorts_full() for c in cohorts: print(f"{c.name} ({c.count} users)") ``` Source code in `src/mixpanel_headless/workspace.py` ```` def list_cohorts_full( self, *, data_group_id: str | None = None, ids: list[int] | None = None, ) -> list[Cohort]: """List cohorts via the App API (full detail). Unlike ``cohorts()`` which uses the discovery endpoint, this method uses the App API and returns full ``Cohort`` objects with all metadata. Args: data_group_id: Optional data group filter. ids: Optional list of cohort IDs to filter by. Returns: List of ``Cohort`` objects. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: API error (400, 404). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() cohorts = ws.list_cohorts_full() for c in cohorts: print(f"{c.name} ({c.count} users)") ``` """ client = self._require_api_client() raw = client.list_cohorts_app(data_group_id=data_group_id, ids=ids) return [Cohort.model_validate(c) for c in raw] ```` ### get_cohort ``` get_cohort(cohort_id: int) -> Cohort ``` Get a single cohort by ID via the App API. | PARAMETER | DESCRIPTION | | ----------- | ---------------------------------- | | `cohort_id` | Cohort identifier. **TYPE:** `int` | | RETURNS | DESCRIPTION | | -------- | ------------------ | | `Cohort` | The Cohort object. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Cohort not found (404). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() cohort = ws.get_cohort(12345) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def get_cohort(self, cohort_id: int) -> Cohort: """Get a single cohort by ID via the App API. Args: cohort_id: Cohort identifier. Returns: The ``Cohort`` object. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Cohort not found (404). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() cohort = ws.get_cohort(12345) ``` """ client = self._require_api_client() raw = client.get_cohort(cohort_id) if raw is None: raise MixpanelHeadlessError( "API returned empty response for get_cohort", ) return Cohort.model_validate(raw) ```` ### create_cohort ``` create_cohort(params: CreateCohortParams) -> Cohort ``` Create a new cohort. | PARAMETER | DESCRIPTION | | --------- | ---------------------------------------------------------- | | `params` | Cohort creation parameters. **TYPE:** `CreateCohortParams` | | RETURNS | DESCRIPTION | | -------- | ------------------------- | | `Cohort` | The newly created Cohort. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Invalid parameters (400). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() cohort = ws.create_cohort( CreateCohortParams(name="Power Users") ) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def create_cohort(self, params: CreateCohortParams) -> Cohort: """Create a new cohort. Args: params: Cohort creation parameters. Returns: The newly created ``Cohort``. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Invalid parameters (400). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() cohort = ws.create_cohort( CreateCohortParams(name="Power Users") ) ``` """ client = self._require_api_client() raw = client.create_cohort(params.model_dump(exclude_none=True)) if raw is None: raise MixpanelHeadlessError( "API returned empty response for create_cohort", ) return Cohort.model_validate(raw) ```` ### update_cohort ``` update_cohort(cohort_id: int, params: UpdateCohortParams) -> Cohort ``` Update an existing cohort. | PARAMETER | DESCRIPTION | | ----------- | ------------------------------------------------ | | `cohort_id` | Cohort identifier. **TYPE:** `int` | | `params` | Fields to update. **TYPE:** `UpdateCohortParams` | | RETURNS | DESCRIPTION | | -------- | ------------------- | | `Cohort` | The updated Cohort. | | RAISES | DESCRIPTION | | --------------------- | ---------------------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Cohort not found or invalid params (400, 404). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() updated = ws.update_cohort( 12345, UpdateCohortParams(name="Renamed") ) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def update_cohort(self, cohort_id: int, params: UpdateCohortParams) -> Cohort: """Update an existing cohort. Args: cohort_id: Cohort identifier. params: Fields to update. Returns: The updated ``Cohort``. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Cohort not found or invalid params (400, 404). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() updated = ws.update_cohort( 12345, UpdateCohortParams(name="Renamed") ) ``` """ client = self._require_api_client() raw = client.update_cohort(cohort_id, params.model_dump(exclude_none=True)) if raw is None: raise MixpanelHeadlessError( "API returned empty response for update_cohort", ) return Cohort.model_validate(raw) ```` ### delete_cohort ``` delete_cohort(cohort_id: int) -> None ``` Delete a cohort. | PARAMETER | DESCRIPTION | | ----------- | ---------------------------------- | | `cohort_id` | Cohort identifier. **TYPE:** `int` | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Cohort not found (404). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() ws.delete_cohort(12345) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def delete_cohort(self, cohort_id: int) -> None: """Delete a cohort. Args: cohort_id: Cohort identifier. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Cohort not found (404). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() ws.delete_cohort(12345) ``` """ client = self._require_api_client() client.delete_cohort(cohort_id) ```` ### bulk_delete_cohorts ``` bulk_delete_cohorts(ids: list[int]) -> None ``` Delete multiple cohorts. | PARAMETER | DESCRIPTION | | --------- | --------------------------------------------------- | | `ids` | List of cohort IDs to delete. **TYPE:** `list[int]` | | RAISES | DESCRIPTION | | --------------------- | ------------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | One or more IDs not found (400, 404). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() ws.bulk_delete_cohorts([1, 2, 3]) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def bulk_delete_cohorts(self, ids: list[int]) -> None: """Delete multiple cohorts. Args: ids: List of cohort IDs to delete. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: One or more IDs not found (400, 404). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() ws.bulk_delete_cohorts([1, 2, 3]) ``` """ client = self._require_api_client() client.bulk_delete_cohorts(ids) ```` ### bulk_update_cohorts ``` bulk_update_cohorts(entries: list[BulkUpdateCohortEntry]) -> None ``` Update multiple cohorts. | PARAMETER | DESCRIPTION | | --------- | ---------------------------------------------------------------------- | | `entries` | List of cohort update entries. **TYPE:** `list[BulkUpdateCohortEntry]` | | RAISES | DESCRIPTION | | --------------------- | -------------------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Invalid entries or IDs not found (400, 404). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() ws.bulk_update_cohorts([ BulkUpdateCohortEntry(id=1, name="Renamed"), ]) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def bulk_update_cohorts(self, entries: list[BulkUpdateCohortEntry]) -> None: """Update multiple cohorts. Args: entries: List of cohort update entries. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Invalid entries or IDs not found (400, 404). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() ws.bulk_update_cohorts([ BulkUpdateCohortEntry(id=1, name="Renamed"), ]) ``` """ client = self._require_api_client() client.bulk_update_cohorts([e.model_dump(exclude_none=True) for e in entries]) ```` ### list_feature_flags ``` list_feature_flags(*, include_archived: bool = False) -> list[FeatureFlag] ``` List feature flags for the current project/workspace. | PARAMETER | DESCRIPTION | | ------------------ | ------------------------------------------------------------------------ | | `include_archived` | When True, include archived flags. **TYPE:** `bool` **DEFAULT:** `False` | | RETURNS | DESCRIPTION | | ------------------- | ---------------------------- | | `list[FeatureFlag]` | List of FeatureFlag objects. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | API error (400, 404). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() flags = ws.list_feature_flags() for f in flags: print(f"{f.name} ({f.key})") ``` Source code in `src/mixpanel_headless/workspace.py` ```` def list_feature_flags( self, *, include_archived: bool = False ) -> list[FeatureFlag]: """List feature flags for the current project/workspace. Args: include_archived: When True, include archived flags. Returns: List of ``FeatureFlag`` objects. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: API error (400, 404). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() flags = ws.list_feature_flags() for f in flags: print(f"{f.name} ({f.key})") ``` """ client = self._require_api_client() raw = client.list_feature_flags(include_archived=include_archived) return [FeatureFlag.model_validate(f) for f in raw] ```` ### create_feature_flag ``` create_feature_flag(params: CreateFeatureFlagParams) -> FeatureFlag ``` Create a new feature flag. | PARAMETER | DESCRIPTION | | --------- | ------------------------------------------------------------- | | `params` | Flag creation parameters. **TYPE:** `CreateFeatureFlagParams` | | RETURNS | DESCRIPTION | | ------------- | ------------------------------ | | `FeatureFlag` | The newly created FeatureFlag. | | RAISES | DESCRIPTION | | --------------------- | ------------------------------------------ | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Duplicate key or invalid parameters (400). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() flag = ws.create_feature_flag( CreateFeatureFlagParams(name="Dark Mode", key="dark_mode") ) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def create_feature_flag(self, params: CreateFeatureFlagParams) -> FeatureFlag: """Create a new feature flag. Args: params: Flag creation parameters. Returns: The newly created ``FeatureFlag``. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Duplicate key or invalid parameters (400). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() flag = ws.create_feature_flag( CreateFeatureFlagParams(name="Dark Mode", key="dark_mode") ) ``` """ client = self._require_api_client() raw = client.create_feature_flag(params.model_dump(exclude_none=True)) if raw is None: raise MixpanelHeadlessError( "API returned empty response for create_feature_flag", ) return FeatureFlag.model_validate(raw) ```` ### get_feature_flag ``` get_feature_flag(flag_id: str) -> FeatureFlag ``` Get a single feature flag by ID. | PARAMETER | DESCRIPTION | | --------- | ---------------------------------- | | `flag_id` | Feature flag UUID. **TYPE:** `str` | | RETURNS | DESCRIPTION | | ------------- | ----------------------- | | `FeatureFlag` | The FeatureFlag object. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Flag not found (404). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() flag = ws.get_feature_flag("abc-123-uuid") ``` Source code in `src/mixpanel_headless/workspace.py` ```` def get_feature_flag(self, flag_id: str) -> FeatureFlag: """Get a single feature flag by ID. Args: flag_id: Feature flag UUID. Returns: The ``FeatureFlag`` object. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Flag not found (404). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() flag = ws.get_feature_flag("abc-123-uuid") ``` """ client = self._require_api_client() raw = client.get_feature_flag(flag_id) if raw is None: raise MixpanelHeadlessError( "API returned empty response for get_feature_flag", ) return FeatureFlag.model_validate(raw) ```` ### update_feature_flag ``` update_feature_flag( flag_id: str, params: UpdateFeatureFlagParams ) -> FeatureFlag ``` Update a feature flag (full replacement, PUT semantics). | PARAMETER | DESCRIPTION | | --------- | ---------------------------------------------------------------- | | `flag_id` | Feature flag UUID. **TYPE:** `str` | | `params` | Complete flag configuration. **TYPE:** `UpdateFeatureFlagParams` | | RETURNS | DESCRIPTION | | ------------- | ------------------------ | | `FeatureFlag` | The updated FeatureFlag. | | RAISES | DESCRIPTION | | --------------------- | -------------------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Flag not found or invalid params (400, 404). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() updated = ws.update_feature_flag( "abc-123", UpdateFeatureFlagParams( name="X", key="x", status=FeatureFlagStatus.ENABLED, ruleset={"variants": []}, ) ) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def update_feature_flag( self, flag_id: str, params: UpdateFeatureFlagParams ) -> FeatureFlag: """Update a feature flag (full replacement, PUT semantics). Args: flag_id: Feature flag UUID. params: Complete flag configuration. Returns: The updated ``FeatureFlag``. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Flag not found or invalid params (400, 404). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() updated = ws.update_feature_flag( "abc-123", UpdateFeatureFlagParams( name="X", key="x", status=FeatureFlagStatus.ENABLED, ruleset={"variants": []}, ) ) ``` """ client = self._require_api_client() raw = client.update_feature_flag(flag_id, params.model_dump(exclude_none=True)) if raw is None: raise MixpanelHeadlessError( "API returned empty response for update_feature_flag", ) return FeatureFlag.model_validate(raw) ```` ### delete_feature_flag ``` delete_feature_flag(flag_id: str) -> None ``` Delete a feature flag. | PARAMETER | DESCRIPTION | | --------- | ---------------------------------- | | `flag_id` | Feature flag UUID. **TYPE:** `str` | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Flag not found (404). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() ws.delete_feature_flag("abc-123-uuid") ``` Source code in `src/mixpanel_headless/workspace.py` ```` def delete_feature_flag(self, flag_id: str) -> None: """Delete a feature flag. Args: flag_id: Feature flag UUID. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Flag not found (404). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() ws.delete_feature_flag("abc-123-uuid") ``` """ client = self._require_api_client() client.delete_feature_flag(flag_id) ```` ### archive_feature_flag ``` archive_feature_flag(flag_id: str) -> None ``` Archive a feature flag (soft-delete). | PARAMETER | DESCRIPTION | | --------- | ---------------------------------- | | `flag_id` | Feature flag UUID. **TYPE:** `str` | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Flag not found (404). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() ws.archive_feature_flag("abc-123-uuid") ``` Source code in `src/mixpanel_headless/workspace.py` ```` def archive_feature_flag(self, flag_id: str) -> None: """Archive a feature flag (soft-delete). Args: flag_id: Feature flag UUID. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Flag not found (404). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() ws.archive_feature_flag("abc-123-uuid") ``` """ client = self._require_api_client() client.archive_feature_flag(flag_id) ```` ### restore_feature_flag ``` restore_feature_flag(flag_id: str) -> FeatureFlag ``` Restore an archived feature flag. | PARAMETER | DESCRIPTION | | --------- | ---------------------------------- | | `flag_id` | Feature flag UUID. **TYPE:** `str` | | RETURNS | DESCRIPTION | | ------------- | ------------------------- | | `FeatureFlag` | The restored FeatureFlag. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Flag not found (404). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() restored = ws.restore_feature_flag("abc-123-uuid") ``` Source code in `src/mixpanel_headless/workspace.py` ```` def restore_feature_flag(self, flag_id: str) -> FeatureFlag: """Restore an archived feature flag. Args: flag_id: Feature flag UUID. Returns: The restored ``FeatureFlag``. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Flag not found (404). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() restored = ws.restore_feature_flag("abc-123-uuid") ``` """ client = self._require_api_client() raw = client.restore_feature_flag(flag_id) return FeatureFlag.model_validate(raw) ```` ### duplicate_feature_flag ``` duplicate_feature_flag(flag_id: str) -> FeatureFlag ``` Duplicate a feature flag. | PARAMETER | DESCRIPTION | | --------- | ---------------------------------- | | `flag_id` | Feature flag UUID. **TYPE:** `str` | | RETURNS | DESCRIPTION | | ------------- | ---------------------------------------- | | `FeatureFlag` | The newly created duplicate FeatureFlag. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Flag not found (404). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() dup = ws.duplicate_feature_flag("abc-123-uuid") ``` Source code in `src/mixpanel_headless/workspace.py` ```` def duplicate_feature_flag(self, flag_id: str) -> FeatureFlag: """Duplicate a feature flag. Args: flag_id: Feature flag UUID. Returns: The newly created duplicate ``FeatureFlag``. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Flag not found (404). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() dup = ws.duplicate_feature_flag("abc-123-uuid") ``` """ client = self._require_api_client() raw = client.duplicate_feature_flag(flag_id) return FeatureFlag.model_validate(raw) ```` ### set_flag_test_users ``` set_flag_test_users(flag_id: str, params: SetTestUsersParams) -> None ``` Set test user variant overrides for a feature flag. | PARAMETER | DESCRIPTION | | --------- | ------------------------------------------------- | | `flag_id` | Feature flag UUID. **TYPE:** `str` | | `params` | Test user mapping. **TYPE:** `SetTestUsersParams` | | RAISES | DESCRIPTION | | --------------------- | ---------------------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Flag not found (404) or invalid payload (400). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() ws.set_flag_test_users( "abc-123", SetTestUsersParams(users={"on": "user-1"}), ) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def set_flag_test_users(self, flag_id: str, params: SetTestUsersParams) -> None: """Set test user variant overrides for a feature flag. Args: flag_id: Feature flag UUID. params: Test user mapping. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Flag not found (404) or invalid payload (400). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() ws.set_flag_test_users( "abc-123", SetTestUsersParams(users={"on": "user-1"}), ) ``` """ client = self._require_api_client() client.set_flag_test_users(flag_id, params.model_dump()) ```` ### get_flag_history ``` get_flag_history( flag_id: str, *, page: str | None = None, page_size: int | None = None ) -> FlagHistoryResponse ``` Get paginated change history for a feature flag. | PARAMETER | DESCRIPTION | | ----------- | ---------------------------------- | | `flag_id` | Feature flag UUID. **TYPE:** `str` | | `page` | Pagination cursor. **TYPE:** \`str | | `page_size` | Results per page. **TYPE:** \`int | | RETURNS | DESCRIPTION | | --------------------- | ------------------------------------------ | | `FlagHistoryResponse` | FlagHistoryResponse with events and count. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Flag not found (404). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() history = ws.get_flag_history("abc-123", page_size=50) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def get_flag_history( self, flag_id: str, *, page: str | None = None, page_size: int | None = None, ) -> FlagHistoryResponse: """Get paginated change history for a feature flag. Args: flag_id: Feature flag UUID. page: Pagination cursor. page_size: Results per page. Returns: ``FlagHistoryResponse`` with events and count. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Flag not found (404). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() history = ws.get_flag_history("abc-123", page_size=50) ``` """ client = self._require_api_client() query_params: dict[str, str] = {} if page is not None: query_params["page"] = page if page_size is not None: query_params["page_size"] = str(page_size) raw = client.get_flag_history( flag_id, params=query_params if query_params else None ) return FlagHistoryResponse.model_validate(raw) ```` ### get_flag_limits ``` get_flag_limits() -> FlagLimitsResponse ``` Get account-level feature flag limits and usage. | RETURNS | DESCRIPTION | | -------------------- | ----------------------------------------------------------------- | | `FlagLimitsResponse` | FlagLimitsResponse with limit, usage, trial, and contract status. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | API error (400). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() limits = ws.get_flag_limits() print(f"{limits.current_usage}/{limits.limit}") ``` Source code in `src/mixpanel_headless/workspace.py` ```` def get_flag_limits(self) -> FlagLimitsResponse: """Get account-level feature flag limits and usage. Returns: ``FlagLimitsResponse`` with limit, usage, trial, and contract status. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: API error (400). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() limits = ws.get_flag_limits() print(f"{limits.current_usage}/{limits.limit}") ``` """ client = self._require_api_client() raw = client.get_flag_limits() return FlagLimitsResponse.model_validate(raw) ```` ### list_experiments ``` list_experiments(*, include_archived: bool = False) -> list[Experiment] ``` List experiments for the current project. | PARAMETER | DESCRIPTION | | ------------------ | ------------------------------------------------------------------------------ | | `include_archived` | When True, include archived experiments. **TYPE:** `bool` **DEFAULT:** `False` | | RETURNS | DESCRIPTION | | ------------------ | --------------------------- | | `list[Experiment]` | List of Experiment objects. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | API error (400, 404). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() experiments = ws.list_experiments() for e in experiments: print(f"{e.name} (status={e.status})") ``` Source code in `src/mixpanel_headless/workspace.py` ```` def list_experiments(self, *, include_archived: bool = False) -> list[Experiment]: """List experiments for the current project. Args: include_archived: When True, include archived experiments. Returns: List of ``Experiment`` objects. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: API error (400, 404). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() experiments = ws.list_experiments() for e in experiments: print(f"{e.name} (status={e.status})") ``` """ client = self._require_api_client() raw = client.list_experiments(include_archived=include_archived) return [Experiment.model_validate(e) for e in raw] ```` ### create_experiment ``` create_experiment(params: CreateExperimentParams) -> Experiment ``` Create a new experiment in Draft status. | PARAMETER | DESCRIPTION | | --------- | ------------------------------------------------------------------ | | `params` | Experiment creation parameters. **TYPE:** `CreateExperimentParams` | | RETURNS | DESCRIPTION | | ------------ | ----------------------------- | | `Experiment` | The newly created Experiment. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Invalid parameters (400). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() exp = ws.create_experiment( CreateExperimentParams(name="Checkout Flow Test") ) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def create_experiment(self, params: CreateExperimentParams) -> Experiment: """Create a new experiment in Draft status. Args: params: Experiment creation parameters. Returns: The newly created ``Experiment``. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Invalid parameters (400). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() exp = ws.create_experiment( CreateExperimentParams(name="Checkout Flow Test") ) ``` """ client = self._require_api_client() raw = client.create_experiment(params.model_dump(exclude_none=True)) if raw is None: raise MixpanelHeadlessError( "API returned empty response for create_experiment", ) return Experiment.model_validate(raw) ```` ### get_experiment ``` get_experiment(experiment_id: str) -> Experiment ``` Get a single experiment by ID. | PARAMETER | DESCRIPTION | | --------------- | -------------------------------- | | `experiment_id` | Experiment UUID. **TYPE:** `str` | | RETURNS | DESCRIPTION | | ------------ | ---------------------- | | `Experiment` | The Experiment object. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Experiment not found (404). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() exp = ws.get_experiment("xyz-456-uuid") ``` Source code in `src/mixpanel_headless/workspace.py` ```` def get_experiment(self, experiment_id: str) -> Experiment: """Get a single experiment by ID. Args: experiment_id: Experiment UUID. Returns: The ``Experiment`` object. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Experiment not found (404). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() exp = ws.get_experiment("xyz-456-uuid") ``` """ client = self._require_api_client() raw = client.get_experiment(experiment_id) if raw is None: raise MixpanelHeadlessError( "API returned empty response for get_experiment", ) return Experiment.model_validate(raw) ```` ### update_experiment ``` update_experiment( experiment_id: str, params: UpdateExperimentParams ) -> Experiment ``` Update an experiment (PATCH semantics). | PARAMETER | DESCRIPTION | | --------------- | ---------------------------------------------------- | | `experiment_id` | Experiment UUID. **TYPE:** `str` | | `params` | Fields to update. **TYPE:** `UpdateExperimentParams` | | RETURNS | DESCRIPTION | | ------------ | ----------------------- | | `Experiment` | The updated Experiment. | | RAISES | DESCRIPTION | | --------------------- | -------------------------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Experiment not found or invalid params (400, 404). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() updated = ws.update_experiment( "xyz-456", UpdateExperimentParams(description="Updated") ) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def update_experiment( self, experiment_id: str, params: UpdateExperimentParams ) -> Experiment: """Update an experiment (PATCH semantics). Args: experiment_id: Experiment UUID. params: Fields to update. Returns: The updated ``Experiment``. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Experiment not found or invalid params (400, 404). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() updated = ws.update_experiment( "xyz-456", UpdateExperimentParams(description="Updated") ) ``` """ client = self._require_api_client() raw = client.update_experiment( experiment_id, params.model_dump(exclude_none=True) ) if raw is None: raise MixpanelHeadlessError( "API returned empty response for update_experiment", ) return Experiment.model_validate(raw) ```` ### delete_experiment ``` delete_experiment(experiment_id: str) -> None ``` Delete an experiment. | PARAMETER | DESCRIPTION | | --------------- | -------------------------------- | | `experiment_id` | Experiment UUID. **TYPE:** `str` | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Experiment not found (404). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() ws.delete_experiment("xyz-456-uuid") ``` Source code in `src/mixpanel_headless/workspace.py` ```` def delete_experiment(self, experiment_id: str) -> None: """Delete an experiment. Args: experiment_id: Experiment UUID. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Experiment not found (404). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() ws.delete_experiment("xyz-456-uuid") ``` """ client = self._require_api_client() client.delete_experiment(experiment_id) ```` ### launch_experiment ``` launch_experiment(experiment_id: str) -> Experiment ``` Launch an experiment (Draft β†’ Active). | PARAMETER | DESCRIPTION | | --------------- | -------------------------------- | | `experiment_id` | Experiment UUID. **TYPE:** `str` | | RETURNS | DESCRIPTION | | ------------ | -------------------------------------------- | | `Experiment` | The launched Experiment with updated status. | | RAISES | DESCRIPTION | | --------------------- | -------------------------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Invalid state transition (400) or not found (404). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() launched = ws.launch_experiment("xyz-456-uuid") ``` Source code in `src/mixpanel_headless/workspace.py` ```` def launch_experiment(self, experiment_id: str) -> Experiment: """Launch an experiment (Draft β†’ Active). Args: experiment_id: Experiment UUID. Returns: The launched ``Experiment`` with updated status. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Invalid state transition (400) or not found (404). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() launched = ws.launch_experiment("xyz-456-uuid") ``` """ client = self._require_api_client() raw = client.launch_experiment(experiment_id) return Experiment.model_validate(raw) ```` ### conclude_experiment ``` conclude_experiment( experiment_id: str, *, params: ExperimentConcludeParams | None = None ) -> Experiment ``` Conclude an experiment (Active β†’ Concluded). Always sends a JSON body (empty `{}` if no params). | PARAMETER | DESCRIPTION | | --------------- | ------------------------------------------------------------------------------------------- | | `experiment_id` | Experiment UUID. **TYPE:** `str` | | `params` | Optional conclude parameters (e.g. end date override). **TYPE:** \`ExperimentConcludeParams | | RETURNS | DESCRIPTION | | ------------ | ------------------------- | | `Experiment` | The concluded Experiment. | | RAISES | DESCRIPTION | | --------------------- | -------------------------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Invalid state transition (400) or not found (404). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() concluded = ws.conclude_experiment("xyz-456-uuid") ``` Source code in `src/mixpanel_headless/workspace.py` ```` def conclude_experiment( self, experiment_id: str, *, params: ExperimentConcludeParams | None = None, ) -> Experiment: """Conclude an experiment (Active β†’ Concluded). Always sends a JSON body (empty ``{}`` if no params). Args: experiment_id: Experiment UUID. params: Optional conclude parameters (e.g. end date override). Returns: The concluded ``Experiment``. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Invalid state transition (400) or not found (404). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() concluded = ws.conclude_experiment("xyz-456-uuid") ``` """ client = self._require_api_client() body = params.model_dump(exclude_none=True) if params else {} raw = client.conclude_experiment(experiment_id, body) return Experiment.model_validate(raw) ```` ### decide_experiment ``` decide_experiment( experiment_id: str, params: ExperimentDecideParams ) -> Experiment ``` Record the experiment decision (Concluded β†’ Success/Fail). | PARAMETER | DESCRIPTION | | --------------- | ----------------------------------------------------------------------------------- | | `experiment_id` | Experiment UUID. **TYPE:** `str` | | `params` | Decision parameters (success, variant, message). **TYPE:** `ExperimentDecideParams` | | RETURNS | DESCRIPTION | | ------------ | -------------------------------------------- | | `Experiment` | The decided Experiment with terminal status. | | RAISES | DESCRIPTION | | --------------------- | -------------------------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Invalid state transition (400) or not found (404). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() decided = ws.decide_experiment( "xyz-456", ExperimentDecideParams(success=True, variant="simplified"), ) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def decide_experiment( self, experiment_id: str, params: ExperimentDecideParams ) -> Experiment: """Record the experiment decision (Concluded β†’ Success/Fail). Args: experiment_id: Experiment UUID. params: Decision parameters (success, variant, message). Returns: The decided ``Experiment`` with terminal status. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Invalid state transition (400) or not found (404). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() decided = ws.decide_experiment( "xyz-456", ExperimentDecideParams(success=True, variant="simplified"), ) ``` """ client = self._require_api_client() raw = client.decide_experiment( experiment_id, params.model_dump(exclude_none=True) ) return Experiment.model_validate(raw) ```` ### archive_experiment ``` archive_experiment(experiment_id: str) -> None ``` Archive an experiment. | PARAMETER | DESCRIPTION | | --------------- | -------------------------------- | | `experiment_id` | Experiment UUID. **TYPE:** `str` | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Experiment not found (404). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() ws.archive_experiment("xyz-456-uuid") ``` Source code in `src/mixpanel_headless/workspace.py` ```` def archive_experiment(self, experiment_id: str) -> None: """Archive an experiment. Args: experiment_id: Experiment UUID. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Experiment not found (404). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() ws.archive_experiment("xyz-456-uuid") ``` """ client = self._require_api_client() client.archive_experiment(experiment_id) ```` ### restore_experiment ``` restore_experiment(experiment_id: str) -> Experiment ``` Restore an archived experiment. | PARAMETER | DESCRIPTION | | --------------- | -------------------------------- | | `experiment_id` | Experiment UUID. **TYPE:** `str` | | RETURNS | DESCRIPTION | | ------------ | ------------------------ | | `Experiment` | The restored Experiment. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Experiment not found (404). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() restored = ws.restore_experiment("xyz-456-uuid") ``` Source code in `src/mixpanel_headless/workspace.py` ```` def restore_experiment(self, experiment_id: str) -> Experiment: """Restore an archived experiment. Args: experiment_id: Experiment UUID. Returns: The restored ``Experiment``. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Experiment not found (404). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() restored = ws.restore_experiment("xyz-456-uuid") ``` """ client = self._require_api_client() raw = client.restore_experiment(experiment_id) return Experiment.model_validate(raw) ```` ### duplicate_experiment ``` duplicate_experiment( experiment_id: str, params: DuplicateExperimentParams ) -> Experiment ``` Duplicate an experiment. A name is required because the Mixpanel API returns an empty response body when duplicating without one. | PARAMETER | DESCRIPTION | | --------------- | -------------------------------------------------------------------------------- | | `experiment_id` | Experiment UUID. **TYPE:** `str` | | `params` | Duplication parameters (name is required). **TYPE:** `DuplicateExperimentParams` | | RETURNS | DESCRIPTION | | ------------ | --------------------------------------- | | `Experiment` | The newly created duplicate Experiment. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Experiment not found (404). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() dup = ws.duplicate_experiment( "xyz-456-uuid", DuplicateExperimentParams(name="Copy"), ) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def duplicate_experiment( self, experiment_id: str, params: DuplicateExperimentParams, ) -> Experiment: """Duplicate an experiment. A name is required because the Mixpanel API returns an empty response body when duplicating without one. Args: experiment_id: Experiment UUID. params: Duplication parameters (``name`` is required). Returns: The newly created duplicate ``Experiment``. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Experiment not found (404). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() dup = ws.duplicate_experiment( "xyz-456-uuid", DuplicateExperimentParams(name="Copy"), ) ``` """ client = self._require_api_client() body = params.model_dump(exclude_none=True) raw = client.duplicate_experiment(experiment_id, body) return Experiment.model_validate(raw) ```` ### list_erf_experiments ``` list_erf_experiments() -> list[dict[str, Any]] ``` List experiments in ERF (Experiment Results Framework) format. | RETURNS | DESCRIPTION | | ---------------------- | --------------------------------------- | | `list[dict[str, Any]]` | List of experiment dicts in ERF format. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | API error (400). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() erf = ws.list_erf_experiments() ``` Source code in `src/mixpanel_headless/workspace.py` ```` def list_erf_experiments(self) -> list[dict[str, Any]]: """List experiments in ERF (Experiment Results Framework) format. Returns: List of experiment dicts in ERF format. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: API error (400). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() erf = ws.list_erf_experiments() ``` """ client = self._require_api_client() return client.list_erf_experiments() ```` ### list_alerts ``` list_alerts( *, bookmark_id: int | None = None, skip_user_filter: bool | None = None ) -> list[CustomAlert] ``` List custom alerts for the current project. | PARAMETER | DESCRIPTION | | ------------------ | ---------------------------------------------------- | | `bookmark_id` | Filter alerts by linked bookmark ID. **TYPE:** \`int | | `skip_user_filter` | If True, list alerts for all users. **TYPE:** \`bool | | RETURNS | DESCRIPTION | | ------------------- | ---------------------------- | | `list[CustomAlert]` | List of CustomAlert objects. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | API error (400). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() alerts = ws.list_alerts() for alert in alerts: print(f"{alert.name} (paused={alert.paused})") ``` Source code in `src/mixpanel_headless/workspace.py` ```` def list_alerts( self, *, bookmark_id: int | None = None, skip_user_filter: bool | None = None, ) -> list[CustomAlert]: """List custom alerts for the current project. Args: bookmark_id: Filter alerts by linked bookmark ID. skip_user_filter: If True, list alerts for all users. Returns: List of ``CustomAlert`` objects. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: API error (400). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() alerts = ws.list_alerts() for alert in alerts: print(f"{alert.name} (paused={alert.paused})") ``` """ client = self._require_api_client() raw_list = client.list_alerts( bookmark_id=bookmark_id, skip_user_filter=skip_user_filter ) return [CustomAlert.model_validate(item) for item in raw_list] ```` ### create_alert ``` create_alert(params: CreateAlertParams) -> CustomAlert ``` Create a new custom alert. | PARAMETER | DESCRIPTION | | --------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | `params` | Alert creation parameters (bookmark_id, name, condition, frequency, paused, and subscriptions are required). **TYPE:** `CreateAlertParams` | | RETURNS | DESCRIPTION | | ------------- | ------------------------ | | `CustomAlert` | The created CustomAlert. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Validation error (400). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() alert = ws.create_alert( CreateAlertParams( bookmark_id=123, name="Daily signups drop", condition={"operator": "less_than", "value": 100}, frequency=86400, paused=False, subscriptions=[{"type": "email", "value": "team@co.com"}], ) ) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def create_alert(self, params: CreateAlertParams) -> CustomAlert: """Create a new custom alert. Args: params: Alert creation parameters (bookmark_id, name, condition, frequency, paused, and subscriptions are required). Returns: The created ``CustomAlert``. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Validation error (400). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() alert = ws.create_alert( CreateAlertParams( bookmark_id=123, name="Daily signups drop", condition={"operator": "less_than", "value": 100}, frequency=86400, paused=False, subscriptions=[{"type": "email", "value": "team@co.com"}], ) ) ``` """ client = self._require_api_client() body = params.model_dump(exclude_none=True) raw = client.create_alert(body) return CustomAlert.model_validate(raw) ```` ### get_alert ``` get_alert(alert_id: int) -> CustomAlert ``` Get a single custom alert by ID. | PARAMETER | DESCRIPTION | | ---------- | ----------------------------------- | | `alert_id` | Alert ID (integer). **TYPE:** `int` | | RETURNS | DESCRIPTION | | ------------- | ----------------------- | | `CustomAlert` | The CustomAlert object. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Alert not found (404). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() alert = ws.get_alert(42) print(alert.name) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def get_alert(self, alert_id: int) -> CustomAlert: """Get a single custom alert by ID. Args: alert_id: Alert ID (integer). Returns: The ``CustomAlert`` object. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Alert not found (404). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() alert = ws.get_alert(42) print(alert.name) ``` """ client = self._require_api_client() raw = client.get_alert(alert_id) return CustomAlert.model_validate(raw) ```` ### update_alert ``` update_alert(alert_id: int, params: UpdateAlertParams) -> CustomAlert ``` Update a custom alert (PATCH semantics). | PARAMETER | DESCRIPTION | | ---------- | ----------------------------------------------- | | `alert_id` | Alert ID (integer). **TYPE:** `int` | | `params` | Fields to update. **TYPE:** `UpdateAlertParams` | | RETURNS | DESCRIPTION | | ------------- | ------------------------ | | `CustomAlert` | The updated CustomAlert. | | RAISES | DESCRIPTION | | --------------------- | ------------------------------------------------ | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Alert not found (404) or validation error (400). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() alert = ws.update_alert( 42, UpdateAlertParams(name="Renamed alert") ) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def update_alert(self, alert_id: int, params: UpdateAlertParams) -> CustomAlert: """Update a custom alert (PATCH semantics). Args: alert_id: Alert ID (integer). params: Fields to update. Returns: The updated ``CustomAlert``. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Alert not found (404) or validation error (400). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() alert = ws.update_alert( 42, UpdateAlertParams(name="Renamed alert") ) ``` """ client = self._require_api_client() body = params.model_dump(exclude_none=True) raw = client.update_alert(alert_id, body) return CustomAlert.model_validate(raw) ```` ### delete_alert ``` delete_alert(alert_id: int) -> None ``` Delete a custom alert. | PARAMETER | DESCRIPTION | | ---------- | ----------------------------------- | | `alert_id` | Alert ID (integer). **TYPE:** `int` | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Alert not found (404). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() ws.delete_alert(42) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def delete_alert(self, alert_id: int) -> None: """Delete a custom alert. Args: alert_id: Alert ID (integer). Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Alert not found (404). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() ws.delete_alert(42) ``` """ client = self._require_api_client() client.delete_alert(alert_id) ```` ### bulk_delete_alerts ``` bulk_delete_alerts(ids: list[int]) -> None ``` Bulk-delete custom alerts. | PARAMETER | DESCRIPTION | | --------- | -------------------------------------------------- | | `ids` | List of alert IDs to delete. **TYPE:** `list[int]` | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Validation error (400). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() ws.bulk_delete_alerts([1, 2, 3]) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def bulk_delete_alerts(self, ids: list[int]) -> None: """Bulk-delete custom alerts. Args: ids: List of alert IDs to delete. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Validation error (400). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() ws.bulk_delete_alerts([1, 2, 3]) ``` """ client = self._require_api_client() client.bulk_delete_alerts(ids) ```` ### get_alert_count ``` get_alert_count(*, alert_type: str | None = None) -> AlertCount ``` Get alert count and limits. | PARAMETER | DESCRIPTION | | ------------ | ---------------------------------------------- | | `alert_type` | Optional filter by alert type. **TYPE:** \`str | | RETURNS | DESCRIPTION | | ------------ | ------------------------------------------------- | | `AlertCount` | AlertCount with count, limit, and is_below_limit. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | API error (400). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() count = ws.get_alert_count() if count.is_below_limit: print(f"{count.anomaly_alerts_count}/{count.alert_limit}") ``` Source code in `src/mixpanel_headless/workspace.py` ```` def get_alert_count(self, *, alert_type: str | None = None) -> AlertCount: """Get alert count and limits. Args: alert_type: Optional filter by alert type. Returns: ``AlertCount`` with count, limit, and is_below_limit. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: API error (400). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() count = ws.get_alert_count() if count.is_below_limit: print(f"{count.anomaly_alerts_count}/{count.alert_limit}") ``` """ client = self._require_api_client() raw = client.get_alert_count(alert_type=alert_type) return AlertCount.model_validate(raw) ```` ### get_alert_history ``` get_alert_history( alert_id: int, *, page_size: int | None = None, next_cursor: str | None = None, previous_cursor: str | None = None, ) -> AlertHistoryResponse ``` Get alert trigger history (paginated). | PARAMETER | DESCRIPTION | | ----------------- | --------------------------------------------- | | `alert_id` | Alert ID (integer). **TYPE:** `int` | | `page_size` | Number of results per page. **TYPE:** \`int | | `next_cursor` | Cursor for the next page. **TYPE:** \`str | | `previous_cursor` | Cursor for the previous page. **TYPE:** \`str | | RETURNS | DESCRIPTION | | ---------------------- | ---------------------------------------------------------- | | `AlertHistoryResponse` | AlertHistoryResponse with results and pagination metadata. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Alert not found (404). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() history = ws.get_alert_history(42, page_size=10) for entry in history.results: print(entry) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def get_alert_history( self, alert_id: int, *, page_size: int | None = None, next_cursor: str | None = None, previous_cursor: str | None = None, ) -> AlertHistoryResponse: """Get alert trigger history (paginated). Args: alert_id: Alert ID (integer). page_size: Number of results per page. next_cursor: Cursor for the next page. previous_cursor: Cursor for the previous page. Returns: ``AlertHistoryResponse`` with results and pagination metadata. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Alert not found (404). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() history = ws.get_alert_history(42, page_size=10) for entry in history.results: print(entry) ``` """ client = self._require_api_client() raw = client.get_alert_history( alert_id, page_size=page_size, next_cursor=next_cursor, previous_cursor=previous_cursor, ) return AlertHistoryResponse.model_validate(raw) ```` ### test_alert ``` test_alert(params: CreateAlertParams) -> dict[str, Any] ``` Send a test alert notification. | PARAMETER | DESCRIPTION | | --------- | ----------------------------------------------------------------------------------- | | `params` | Alert parameters for the test (same shape as create). **TYPE:** `CreateAlertParams` | | RETURNS | DESCRIPTION | | ---------------- | ----------------------------------------------------- | | `dict[str, Any]` | Dictionary with test result status (opaque response). | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Validation error (400). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() result = ws.test_alert( CreateAlertParams( bookmark_id=123, name="Test", condition={}, frequency=86400, paused=False, subscriptions=[], ) ) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def test_alert(self, params: CreateAlertParams) -> dict[str, Any]: """Send a test alert notification. Args: params: Alert parameters for the test (same shape as create). Returns: Dictionary with test result status (opaque response). Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Validation error (400). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() result = ws.test_alert( CreateAlertParams( bookmark_id=123, name="Test", condition={}, frequency=86400, paused=False, subscriptions=[], ) ) ``` """ client = self._require_api_client() body = params.model_dump(exclude_none=True) return client.test_alert(body) ```` ### get_alert_screenshot_url ``` get_alert_screenshot_url(gcs_key: str) -> AlertScreenshotResponse ``` Get a signed URL for an alert screenshot. | PARAMETER | DESCRIPTION | | --------- | -------------------------------------------------- | | `gcs_key` | GCS object key for the screenshot. **TYPE:** `str` | | RETURNS | DESCRIPTION | | ------------------------- | -------------------------------------------- | | `AlertScreenshotResponse` | AlertScreenshotResponse with the signed URL. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Screenshot not found (404). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() resp = ws.get_alert_screenshot_url("screenshots/abc.png") print(resp.signed_url) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def get_alert_screenshot_url(self, gcs_key: str) -> AlertScreenshotResponse: """Get a signed URL for an alert screenshot. Args: gcs_key: GCS object key for the screenshot. Returns: ``AlertScreenshotResponse`` with the signed URL. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Screenshot not found (404). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() resp = ws.get_alert_screenshot_url("screenshots/abc.png") print(resp.signed_url) ``` """ client = self._require_api_client() raw = client.get_alert_screenshot_url(gcs_key) return AlertScreenshotResponse.model_validate(raw) ```` ### validate_alerts_for_bookmark ``` validate_alerts_for_bookmark( params: ValidateAlertsForBookmarkParams, ) -> ValidateAlertsForBookmarkResponse ``` Validate alerts against a bookmark configuration. | PARAMETER | DESCRIPTION | | --------- | --------------------------------------------------------------------------------------------------------------------------- | | `params` | Validation parameters (alert_ids, bookmark_type, bookmark_params are required). **TYPE:** `ValidateAlertsForBookmarkParams` | | RETURNS | DESCRIPTION | | ----------------------------------- | ------------------------------------------------------------ | | `ValidateAlertsForBookmarkResponse` | ValidateAlertsForBookmarkResponse with per-alert validations | | `ValidateAlertsForBookmarkResponse` | and invalid count. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Validation error (400). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() resp = ws.validate_alerts_for_bookmark( ValidateAlertsForBookmarkParams( alert_ids=[1, 2], bookmark_type="insights", bookmark_params={"event": "Signup"}, ) ) if resp.invalid_count > 0: for v in resp.alert_validations: if not v.valid: print(f"{v.alert_name}: {v.reason}") ``` Source code in `src/mixpanel_headless/workspace.py` ```` def validate_alerts_for_bookmark( self, params: ValidateAlertsForBookmarkParams ) -> ValidateAlertsForBookmarkResponse: """Validate alerts against a bookmark configuration. Args: params: Validation parameters (alert_ids, bookmark_type, bookmark_params are required). Returns: ``ValidateAlertsForBookmarkResponse`` with per-alert validations and invalid count. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Validation error (400). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() resp = ws.validate_alerts_for_bookmark( ValidateAlertsForBookmarkParams( alert_ids=[1, 2], bookmark_type="insights", bookmark_params={"event": "Signup"}, ) ) if resp.invalid_count > 0: for v in resp.alert_validations: if not v.valid: print(f"{v.alert_name}: {v.reason}") ``` """ client = self._require_api_client() body = params.model_dump(exclude_none=True) raw = client.validate_alerts_for_bookmark(body) return ValidateAlertsForBookmarkResponse.model_validate(raw) ```` ### list_annotations ``` list_annotations( *, from_date: str | None = None, to_date: str | None = None, tags: list[int] | None = None, ) -> list[Annotation] ``` List timeline annotations for the project. | PARAMETER | DESCRIPTION | | ----------- | ------------------------------------------------------------------ | | `from_date` | Start date filter (ISO format, e.g. "2026-01-01"). **TYPE:** \`str | | `to_date` | End date filter (ISO format, e.g. "2026-03-31"). **TYPE:** \`str | | `tags` | Tag IDs to filter by. **TYPE:** \`list[int] | | RETURNS | DESCRIPTION | | ------------------ | --------------------------- | | `list[Annotation]` | List of Annotation objects. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | API error (400). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() annotations = ws.list_annotations(from_date="2026-01-01") for ann in annotations: print(f"{ann.date}: {ann.description}") ``` Source code in `src/mixpanel_headless/workspace.py` ```` def list_annotations( self, *, from_date: str | None = None, to_date: str | None = None, tags: list[int] | None = None, ) -> list[Annotation]: """List timeline annotations for the project. Args: from_date: Start date filter (ISO format, e.g. ``"2026-01-01"``). to_date: End date filter (ISO format, e.g. ``"2026-03-31"``). tags: Tag IDs to filter by. Returns: List of ``Annotation`` objects. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: API error (400). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() annotations = ws.list_annotations(from_date="2026-01-01") for ann in annotations: print(f"{ann.date}: {ann.description}") ``` """ client = self._require_api_client() raw_list = client.list_annotations( from_date=from_date, to_date=to_date, tags=tags ) return [Annotation.model_validate(item) for item in raw_list] ```` ### create_annotation ``` create_annotation(params: CreateAnnotationParams) -> Annotation ``` Create a new timeline annotation. | PARAMETER | DESCRIPTION | | --------- | ----------------------------------------------------------------------------------------------- | | `params` | Annotation creation parameters (date, description required). **TYPE:** `CreateAnnotationParams` | | RETURNS | DESCRIPTION | | ------------ | ----------------------- | | `Annotation` | The created Annotation. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Validation error (400). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() ann = ws.create_annotation( CreateAnnotationParams( date="2026-03-31", description="v2.5 release" ) ) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def create_annotation(self, params: CreateAnnotationParams) -> Annotation: """Create a new timeline annotation. Args: params: Annotation creation parameters (date, description required). Returns: The created ``Annotation``. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Validation error (400). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() ann = ws.create_annotation( CreateAnnotationParams( date="2026-03-31", description="v2.5 release" ) ) ``` """ client = self._require_api_client() body = params.model_dump(exclude_none=True) raw = client.create_annotation(body) return Annotation.model_validate(raw) ```` ### get_annotation ``` get_annotation(annotation_id: int) -> Annotation ``` Get a single annotation by ID. | PARAMETER | DESCRIPTION | | --------------- | ------------------------------ | | `annotation_id` | Annotation ID. **TYPE:** `int` | | RETURNS | DESCRIPTION | | ------------ | ---------------------- | | `Annotation` | The Annotation object. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Annotation not found (404). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() ann = ws.get_annotation(42) print(ann.description) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def get_annotation(self, annotation_id: int) -> Annotation: """Get a single annotation by ID. Args: annotation_id: Annotation ID. Returns: The ``Annotation`` object. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Annotation not found (404). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() ann = ws.get_annotation(42) print(ann.description) ``` """ client = self._require_api_client() raw = client.get_annotation(annotation_id) return Annotation.model_validate(raw) ```` ### update_annotation ``` update_annotation( annotation_id: int, params: UpdateAnnotationParams ) -> Annotation ``` Update an annotation (PATCH semantics). | PARAMETER | DESCRIPTION | | --------------- | ------------------------------------------------------------------------ | | `annotation_id` | Annotation ID. **TYPE:** `int` | | `params` | Fields to update (description, tags). **TYPE:** `UpdateAnnotationParams` | | RETURNS | DESCRIPTION | | ------------ | ----------------------- | | `Annotation` | The updated Annotation. | | RAISES | DESCRIPTION | | --------------------- | ----------------------------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Annotation not found (404) or validation error (400). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() ann = ws.update_annotation( 42, UpdateAnnotationParams(description="Updated text") ) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def update_annotation( self, annotation_id: int, params: UpdateAnnotationParams ) -> Annotation: """Update an annotation (PATCH semantics). Args: annotation_id: Annotation ID. params: Fields to update (description, tags). Returns: The updated ``Annotation``. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Annotation not found (404) or validation error (400). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() ann = ws.update_annotation( 42, UpdateAnnotationParams(description="Updated text") ) ``` """ client = self._require_api_client() body = params.model_dump(exclude_none=True) raw = client.update_annotation(annotation_id, body) return Annotation.model_validate(raw) ```` ### delete_annotation ``` delete_annotation(annotation_id: int) -> None ``` Delete an annotation. | PARAMETER | DESCRIPTION | | --------------- | ------------------------------ | | `annotation_id` | Annotation ID. **TYPE:** `int` | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Annotation not found (404). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() ws.delete_annotation(42) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def delete_annotation(self, annotation_id: int) -> None: """Delete an annotation. Args: annotation_id: Annotation ID. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Annotation not found (404). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() ws.delete_annotation(42) ``` """ client = self._require_api_client() client.delete_annotation(annotation_id) ```` ### list_annotation_tags ``` list_annotation_tags() -> list[AnnotationTag] ``` List annotation tags for the project. | RETURNS | DESCRIPTION | | --------------------- | ------------------------------ | | `list[AnnotationTag]` | List of AnnotationTag objects. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | API error (400). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() tags = ws.list_annotation_tags() for tag in tags: print(tag.name) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def list_annotation_tags(self) -> list[AnnotationTag]: """List annotation tags for the project. Returns: List of ``AnnotationTag`` objects. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: API error (400). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() tags = ws.list_annotation_tags() for tag in tags: print(tag.name) ``` """ client = self._require_api_client() raw_list = client.list_annotation_tags() return [AnnotationTag.model_validate(item) for item in raw_list] ```` ### create_annotation_tag ``` create_annotation_tag(params: CreateAnnotationTagParams) -> AnnotationTag ``` Create a new annotation tag. | PARAMETER | DESCRIPTION | | --------- | ------------------------------------------------------------------------------ | | `params` | Tag creation parameters (name required). **TYPE:** `CreateAnnotationTagParams` | | RETURNS | DESCRIPTION | | --------------- | -------------------------- | | `AnnotationTag` | The created AnnotationTag. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Validation error (400). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() tag = ws.create_annotation_tag( CreateAnnotationTagParams(name="releases") ) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def create_annotation_tag(self, params: CreateAnnotationTagParams) -> AnnotationTag: """Create a new annotation tag. Args: params: Tag creation parameters (name required). Returns: The created ``AnnotationTag``. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Validation error (400). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() tag = ws.create_annotation_tag( CreateAnnotationTagParams(name="releases") ) ``` """ client = self._require_api_client() body = params.model_dump(exclude_none=True) raw = client.create_annotation_tag(body) return AnnotationTag.model_validate(raw) ```` ### list_webhooks ``` list_webhooks() -> list[ProjectWebhook] ``` List all webhooks for the current project. | RETURNS | DESCRIPTION | | ---------------------- | ------------------------------- | | `list[ProjectWebhook]` | List of ProjectWebhook objects. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | API error (400). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() webhooks = ws.list_webhooks() for wh in webhooks: print(f"{wh.name} -> {wh.url}") ``` Source code in `src/mixpanel_headless/workspace.py` ```` def list_webhooks(self) -> list[ProjectWebhook]: """List all webhooks for the current project. Returns: List of ``ProjectWebhook`` objects. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: API error (400). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() webhooks = ws.list_webhooks() for wh in webhooks: print(f"{wh.name} -> {wh.url}") ``` """ client = self._require_api_client() raw_list = client.list_webhooks() return [ProjectWebhook.model_validate(item) for item in raw_list] ```` ### create_webhook ``` create_webhook(params: CreateWebhookParams) -> WebhookMutationResult ``` Create a new webhook. | PARAMETER | DESCRIPTION | | --------- | ------------------------------------------------------------ | | `params` | Webhook creation parameters. **TYPE:** `CreateWebhookParams` | | RETURNS | DESCRIPTION | | ----------------------- | --------------------------------------------------------- | | `WebhookMutationResult` | WebhookMutationResult with the new webhook's id and name. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | API error (400). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() result = ws.create_webhook( CreateWebhookParams(name="Pipeline", url="https://example.com/hook") ) print(result.id) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def create_webhook(self, params: CreateWebhookParams) -> WebhookMutationResult: """Create a new webhook. Args: params: Webhook creation parameters. Returns: ``WebhookMutationResult`` with the new webhook's id and name. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: API error (400). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() result = ws.create_webhook( CreateWebhookParams(name="Pipeline", url="https://example.com/hook") ) print(result.id) ``` """ client = self._require_api_client() body = params.model_dump(exclude_none=True) raw = client.create_webhook(body) return WebhookMutationResult.model_validate(raw) ```` ### update_webhook ``` update_webhook( webhook_id: str, params: UpdateWebhookParams ) -> WebhookMutationResult ``` Update an existing webhook. | PARAMETER | DESCRIPTION | | ------------ | ------------------------------------------------------------------- | | `webhook_id` | Webhook UUID string. **TYPE:** `str` | | `params` | Fields to update (PATCH semantics). **TYPE:** `UpdateWebhookParams` | | RETURNS | DESCRIPTION | | ----------------------- | ------------------------------------------------------------- | | `WebhookMutationResult` | WebhookMutationResult with the updated webhook's id and name. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Webhook not found (404). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() result = ws.update_webhook( "wh-uuid-123", UpdateWebhookParams(name="Renamed Hook"), ) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def update_webhook( self, webhook_id: str, params: UpdateWebhookParams ) -> WebhookMutationResult: """Update an existing webhook. Args: webhook_id: Webhook UUID string. params: Fields to update (PATCH semantics). Returns: ``WebhookMutationResult`` with the updated webhook's id and name. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Webhook not found (404). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() result = ws.update_webhook( "wh-uuid-123", UpdateWebhookParams(name="Renamed Hook"), ) ``` """ client = self._require_api_client() body = params.model_dump(exclude_none=True) raw = client.update_webhook(webhook_id, body) return WebhookMutationResult.model_validate(raw) ```` ### delete_webhook ``` delete_webhook(webhook_id: str) -> None ``` Delete a webhook. | PARAMETER | DESCRIPTION | | ------------ | ------------------------------------ | | `webhook_id` | Webhook UUID string. **TYPE:** `str` | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Webhook not found (404). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() ws.delete_webhook("wh-uuid-123") ``` Source code in `src/mixpanel_headless/workspace.py` ```` def delete_webhook(self, webhook_id: str) -> None: """Delete a webhook. Args: webhook_id: Webhook UUID string. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Webhook not found (404). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() ws.delete_webhook("wh-uuid-123") ``` """ client = self._require_api_client() client.delete_webhook(webhook_id) ```` ### test_webhook ``` test_webhook(params: WebhookTestParams) -> WebhookTestResult ``` Test webhook connectivity. | PARAMETER | DESCRIPTION | | --------- | ------------------------------------------------------------------------ | | `params` | Webhook test parameters (url is required). **TYPE:** `WebhookTestParams` | | RETURNS | DESCRIPTION | | ------------------- | --------------------------------------------------------- | | `WebhookTestResult` | WebhookTestResult with success, status_code, and message. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | API error (400). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() result = ws.test_webhook( WebhookTestParams(url="https://example.com/hook") ) if result.success: print("Webhook is reachable") ``` Source code in `src/mixpanel_headless/workspace.py` ```` def test_webhook(self, params: WebhookTestParams) -> WebhookTestResult: """Test webhook connectivity. Args: params: Webhook test parameters (url is required). Returns: ``WebhookTestResult`` with success, status_code, and message. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: API error (400). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() result = ws.test_webhook( WebhookTestParams(url="https://example.com/hook") ) if result.success: print("Webhook is reachable") ``` """ client = self._require_api_client() body = params.model_dump(exclude_none=True) raw = client.test_webhook(body) return WebhookTestResult.model_validate(raw) ```` ### get_event_definitions ``` get_event_definitions(*, names: list[str]) -> list[EventDefinition] ``` Get event definitions from Lexicon by name. Retrieves metadata (description, tags, visibility, etc.) for the specified events from the Mixpanel Lexicon. | PARAMETER | DESCRIPTION | | --------- | ----------------------------------------------------- | | `names` | List of event names to look up. **TYPE:** `list[str]` | | RETURNS | DESCRIPTION | | ----------------------- | --------------------------------------------------------- | | `list[EventDefinition]` | List of EventDefinition objects for the requested events. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Validation error (400). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() defs = ws.get_event_definitions(names=["Signup", "Login"]) for d in defs: print(f"{d.name}: {d.description}") ``` Source code in `src/mixpanel_headless/workspace.py` ```` def get_event_definitions(self, *, names: list[str]) -> list[EventDefinition]: """Get event definitions from Lexicon by name. Retrieves metadata (description, tags, visibility, etc.) for the specified events from the Mixpanel Lexicon. Args: names: List of event names to look up. Returns: List of ``EventDefinition`` objects for the requested events. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Validation error (400). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() defs = ws.get_event_definitions(names=["Signup", "Login"]) for d in defs: print(f"{d.name}: {d.description}") ``` """ client = self._require_api_client() raw_list = client.get_event_definitions(names) return [EventDefinition.model_validate(x) for x in raw_list] ```` ### update_event_definition ``` update_event_definition( event_name: str, params: UpdateEventDefinitionParams ) -> EventDefinition ``` Update an event definition in Lexicon. | PARAMETER | DESCRIPTION | | ------------ | ---------------------------------------------------------------------------------------------------------------- | | `event_name` | Name of the event to update. **TYPE:** `str` | | `params` | Fields to update (hidden, dropped, merged, verified, tags, description). **TYPE:** `UpdateEventDefinitionParams` | | RETURNS | DESCRIPTION | | ----------------- | ---------------------------- | | `EventDefinition` | The updated EventDefinition. | | RAISES | DESCRIPTION | | --------------------- | ------------------------------------------------ | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Event not found (404) or validation error (400). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() definition = ws.update_event_definition( "Signup", UpdateEventDefinitionParams(description="User signed up"), ) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def update_event_definition( self, event_name: str, params: UpdateEventDefinitionParams ) -> EventDefinition: """Update an event definition in Lexicon. Args: event_name: Name of the event to update. params: Fields to update (hidden, dropped, merged, verified, tags, description). Returns: The updated ``EventDefinition``. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Event not found (404) or validation error (400). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() definition = ws.update_event_definition( "Signup", UpdateEventDefinitionParams(description="User signed up"), ) ``` """ client = self._require_api_client() body = params.model_dump(exclude_none=True) raw = client.update_event_definition(event_name, body) return EventDefinition.model_validate(raw) ```` ### delete_event_definition ``` delete_event_definition(event_name: str) -> None ``` Delete an event definition from Lexicon. | PARAMETER | DESCRIPTION | | ------------ | -------------------------------------------- | | `event_name` | Name of the event to delete. **TYPE:** `str` | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Event not found (404). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() ws.delete_event_definition("OldEvent") ``` Source code in `src/mixpanel_headless/workspace.py` ```` def delete_event_definition(self, event_name: str) -> None: """Delete an event definition from Lexicon. Args: event_name: Name of the event to delete. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Event not found (404). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() ws.delete_event_definition("OldEvent") ``` """ client = self._require_api_client() client.delete_event_definition(event_name) ```` ### bulk_update_event_definitions ``` bulk_update_event_definitions( params: BulkUpdateEventsParams, ) -> list[EventDefinition] ``` Bulk-update event definitions in Lexicon. | PARAMETER | DESCRIPTION | | --------- | ----------------------------------------------------------------------------------------------------------------------- | | `params` | Bulk update parameters containing a list of event updates (name + fields to change). **TYPE:** `BulkUpdateEventsParams` | | RETURNS | DESCRIPTION | | ----------------------- | ---------------------------------------- | | `list[EventDefinition]` | List of updated EventDefinition objects. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Validation error (400). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() defs = ws.bulk_update_event_definitions( BulkUpdateEventsParams(events=[ {"name": "Signup", "description": "User signed up"}, {"name": "Login", "hidden": True}, ]) ) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def bulk_update_event_definitions( self, params: BulkUpdateEventsParams ) -> list[EventDefinition]: """Bulk-update event definitions in Lexicon. Args: params: Bulk update parameters containing a list of event updates (name + fields to change). Returns: List of updated ``EventDefinition`` objects. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Validation error (400). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() defs = ws.bulk_update_event_definitions( BulkUpdateEventsParams(events=[ {"name": "Signup", "description": "User signed up"}, {"name": "Login", "hidden": True}, ]) ) ``` """ client = self._require_api_client() body = params.model_dump(exclude_none=True) raw_list = client.bulk_update_event_definitions(body) return [EventDefinition.model_validate(x) for x in raw_list] ```` ### get_property_definitions ``` get_property_definitions( *, names: list[str], resource_type: str | None = None ) -> list[PropertyDefinition] ``` Get property definitions from Lexicon by name. Retrieves metadata (description, tags, visibility, etc.) for the specified properties from the Mixpanel Lexicon. | PARAMETER | DESCRIPTION | | --------------- | ------------------------------------------------------------------------------------- | | `names` | List of property names to look up. **TYPE:** `list[str]` | | `resource_type` | Optional resource type filter (e.g. "event", "user", "groupprofile"). **TYPE:** \`str | | RETURNS | DESCRIPTION | | -------------------------- | ---------------------------------------------------------------- | | `list[PropertyDefinition]` | List of PropertyDefinition objects for the requested properties. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Validation error (400). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() defs = ws.get_property_definitions( names=["plan_type", "country"], resource_type="event", ) for d in defs: print(f"{d.name}: {d.description}") ``` Source code in `src/mixpanel_headless/workspace.py` ```` def get_property_definitions( self, *, names: list[str], resource_type: str | None = None, ) -> list[PropertyDefinition]: """Get property definitions from Lexicon by name. Retrieves metadata (description, tags, visibility, etc.) for the specified properties from the Mixpanel Lexicon. Args: names: List of property names to look up. resource_type: Optional resource type filter (e.g. ``"event"``, ``"user"``, ``"groupprofile"``). Returns: List of ``PropertyDefinition`` objects for the requested properties. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Validation error (400). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() defs = ws.get_property_definitions( names=["plan_type", "country"], resource_type="event", ) for d in defs: print(f"{d.name}: {d.description}") ``` """ client = self._require_api_client() raw_list = client.get_property_definitions(names, resource_type=resource_type) return [PropertyDefinition.model_validate(x) for x in raw_list] ```` ### update_property_definition ``` update_property_definition( property_name: str, params: UpdatePropertyDefinitionParams ) -> PropertyDefinition ``` Update a property definition in Lexicon. | PARAMETER | DESCRIPTION | | --------------- | -------------------------------------------------------------------------------------------------------------- | | `property_name` | Name of the property to update. **TYPE:** `str` | | `params` | Fields to update (hidden, dropped, merged, sensitive, description). **TYPE:** `UpdatePropertyDefinitionParams` | | RETURNS | DESCRIPTION | | -------------------- | ------------------------------- | | `PropertyDefinition` | The updated PropertyDefinition. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Property not found (404) or validation error (400). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() definition = ws.update_property_definition( "plan_type", UpdatePropertyDefinitionParams(description="User plan tier"), ) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def update_property_definition( self, property_name: str, params: UpdatePropertyDefinitionParams ) -> PropertyDefinition: """Update a property definition in Lexicon. Args: property_name: Name of the property to update. params: Fields to update (hidden, dropped, merged, sensitive, description). Returns: The updated ``PropertyDefinition``. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Property not found (404) or validation error (400). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() definition = ws.update_property_definition( "plan_type", UpdatePropertyDefinitionParams(description="User plan tier"), ) ``` """ client = self._require_api_client() body = params.model_dump(exclude_none=True) raw = client.update_property_definition(property_name, body) return PropertyDefinition.model_validate(raw) ```` ### bulk_update_property_definitions ``` bulk_update_property_definitions( params: BulkUpdatePropertiesParams, ) -> list[PropertyDefinition] ``` Bulk-update property definitions in Lexicon. | PARAMETER | DESCRIPTION | | --------- | ------------------------------------------------------------------------------------------------------------------------------ | | `params` | Bulk update parameters containing a list of property updates (name + fields to change). **TYPE:** `BulkUpdatePropertiesParams` | | RETURNS | DESCRIPTION | | -------------------------- | ------------------------------------------- | | `list[PropertyDefinition]` | List of updated PropertyDefinition objects. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Validation error (400). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() defs = ws.bulk_update_property_definitions( BulkUpdatePropertiesParams(properties=[ {"name": "plan_type", "description": "User plan tier"}, {"name": "country", "hidden": True}, ]) ) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def bulk_update_property_definitions( self, params: BulkUpdatePropertiesParams ) -> list[PropertyDefinition]: """Bulk-update property definitions in Lexicon. Args: params: Bulk update parameters containing a list of property updates (name + fields to change). Returns: List of updated ``PropertyDefinition`` objects. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Validation error (400). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() defs = ws.bulk_update_property_definitions( BulkUpdatePropertiesParams(properties=[ {"name": "plan_type", "description": "User plan tier"}, {"name": "country", "hidden": True}, ]) ) ``` """ client = self._require_api_client() body = params.model_dump(exclude_none=True, by_alias=True) raw_list = client.bulk_update_property_definitions(body) return [PropertyDefinition.model_validate(x) for x in raw_list] ```` ### list_lexicon_tags ``` list_lexicon_tags() -> list[LexiconTag] ``` List all Lexicon tags. | RETURNS | DESCRIPTION | | ------------------ | --------------------------------------------------- | | `list[LexiconTag]` | List of LexiconTag objects with id and name fields. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() tags = ws.list_lexicon_tags() for tag in tags: print(tag.name) ``` Note The list endpoint may return plain tag name strings without IDs. In that case, `id` is set to `0` as a sentinel value. Do not pass this sentinel to `update_lexicon_tag()` β€” use name-based operations (e.g. `delete_lexicon_tag(tag.name)`) for tags obtained from this method. Source code in `src/mixpanel_headless/workspace.py` ```` def list_lexicon_tags(self) -> list[LexiconTag]: """List all Lexicon tags. Returns: List of ``LexiconTag`` objects with ``id`` and ``name`` fields. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() tags = ws.list_lexicon_tags() for tag in tags: print(tag.name) ``` Note: The list endpoint may return plain tag name strings without IDs. In that case, ``id`` is set to ``0`` as a sentinel value. Do not pass this sentinel to ``update_lexicon_tag()`` β€” use name-based operations (e.g. ``delete_lexicon_tag(tag.name)``) for tags obtained from this method. """ client = self._require_api_client() raw_list = client.list_lexicon_tags() result: list[LexiconTag] = [] for x in raw_list: if isinstance(x, str): # List endpoint returns plain tag name strings (no id); # id=0 is a sentinel β€” see docstring Note. result.append(LexiconTag(id=0, name=x)) else: result.append(LexiconTag.model_validate(x)) return result ```` ### create_lexicon_tag ``` create_lexicon_tag(params: CreateTagParams) -> LexiconTag ``` Create a new Lexicon tag. | PARAMETER | DESCRIPTION | | --------- | ----------------------------------------------------------------------- | | `params` | Tag creation parameters (name is required). **TYPE:** `CreateTagParams` | | RETURNS | DESCRIPTION | | ------------ | ----------------------- | | `LexiconTag` | The created LexiconTag. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Validation error (400) or tag already exists. | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() tag = ws.create_lexicon_tag(CreateTagParams(name="core-events")) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def create_lexicon_tag(self, params: CreateTagParams) -> LexiconTag: """Create a new Lexicon tag. Args: params: Tag creation parameters (name is required). Returns: The created ``LexiconTag``. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Validation error (400) or tag already exists. ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() tag = ws.create_lexicon_tag(CreateTagParams(name="core-events")) ``` """ client = self._require_api_client() body = params.model_dump(exclude_none=True) raw = client.create_lexicon_tag(body) return LexiconTag.model_validate(raw) ```` ### update_lexicon_tag ``` update_lexicon_tag(tag_id: int, params: UpdateTagParams) -> LexiconTag ``` Update a Lexicon tag. | PARAMETER | DESCRIPTION | | --------- | --------------------------------------------------------- | | `tag_id` | Tag ID (integer). **TYPE:** `int` | | `params` | Fields to update (e.g. name). **TYPE:** `UpdateTagParams` | | RETURNS | DESCRIPTION | | ------------ | ----------------------- | | `LexiconTag` | The updated LexiconTag. | | RAISES | DESCRIPTION | | --------------------- | ---------------------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Tag not found (404) or validation error (400). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() tag = ws.update_lexicon_tag( 5, UpdateTagParams(name="renamed-tag") ) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def update_lexicon_tag(self, tag_id: int, params: UpdateTagParams) -> LexiconTag: """Update a Lexicon tag. Args: tag_id: Tag ID (integer). params: Fields to update (e.g. name). Returns: The updated ``LexiconTag``. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Tag not found (404) or validation error (400). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() tag = ws.update_lexicon_tag( 5, UpdateTagParams(name="renamed-tag") ) ``` """ client = self._require_api_client() body = params.model_dump(exclude_none=True) raw = client.update_lexicon_tag(tag_id, body) return LexiconTag.model_validate(raw) ```` ### delete_lexicon_tag ``` delete_lexicon_tag(tag_name: str) -> None ``` Delete a Lexicon tag by name. | PARAMETER | DESCRIPTION | | ---------- | ------------------------------------------ | | `tag_name` | Name of the tag to delete. **TYPE:** `str` | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Tag not found (404). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() ws.delete_lexicon_tag("deprecated-tag") ``` Source code in `src/mixpanel_headless/workspace.py` ```` def delete_lexicon_tag(self, tag_name: str) -> None: """Delete a Lexicon tag by name. Args: tag_name: Name of the tag to delete. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Tag not found (404). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() ws.delete_lexicon_tag("deprecated-tag") ``` """ client = self._require_api_client() client.delete_lexicon_tag(tag_name) ```` ### get_tracking_metadata ``` get_tracking_metadata(event_name: str) -> dict[str, Any] ``` Get tracking metadata for an event. Retrieves information about how an event is being tracked (sources, SDKs, volume, etc.). | PARAMETER | DESCRIPTION | | ------------ | ---------------------------------- | | `event_name` | Name of the event. **TYPE:** `str` | | RETURNS | DESCRIPTION | | ---------------- | --------------------------------- | | `dict[str, Any]` | Raw tracking metadata dictionary. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Event not found (404). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() metadata = ws.get_tracking_metadata("Signup") print(metadata) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def get_tracking_metadata(self, event_name: str) -> dict[str, Any]: """Get tracking metadata for an event. Retrieves information about how an event is being tracked (sources, SDKs, volume, etc.). Args: event_name: Name of the event. Returns: Raw tracking metadata dictionary. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Event not found (404). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() metadata = ws.get_tracking_metadata("Signup") print(metadata) ``` """ client = self._require_api_client() return client.get_tracking_metadata(event_name) ```` ### get_event_history ``` get_event_history(event_name: str) -> list[dict[str, Any]] ``` Get change history for an event definition. | PARAMETER | DESCRIPTION | | ------------ | ---------------------------------- | | `event_name` | Name of the event. **TYPE:** `str` | | RETURNS | DESCRIPTION | | ---------------------- | ---------------------------------------------------------- | | `list[dict[str, Any]]` | List of history entries (raw dictionaries) showing changes | | `list[dict[str, Any]]` | to the event definition over time. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Event not found (404). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() history = ws.get_event_history("Signup") for entry in history: print(f"{entry['timestamp']}: {entry['action']}") ``` Source code in `src/mixpanel_headless/workspace.py` ```` def get_event_history(self, event_name: str) -> list[dict[str, Any]]: """Get change history for an event definition. Args: event_name: Name of the event. Returns: List of history entries (raw dictionaries) showing changes to the event definition over time. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Event not found (404). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() history = ws.get_event_history("Signup") for entry in history: print(f"{entry['timestamp']}: {entry['action']}") ``` """ client = self._require_api_client() return client.get_event_history(event_name) ```` ### get_property_history ``` get_property_history( property_name: str, entity_type: str ) -> list[dict[str, Any]] ``` Get change history for a property definition. | PARAMETER | DESCRIPTION | | --------------- | ------------------------------------------------------------ | | `property_name` | Name of the property. **TYPE:** `str` | | `entity_type` | Entity type (e.g. "event", "user", "group"). **TYPE:** `str` | | RETURNS | DESCRIPTION | | ---------------------- | ---------------------------------------------------------- | | `list[dict[str, Any]]` | List of history entries (raw dictionaries) showing changes | | `list[dict[str, Any]]` | to the property definition over time. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Property not found (404). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() history = ws.get_property_history("plan_type", "event") for entry in history: print(f"{entry['timestamp']}: {entry['action']}") ``` Source code in `src/mixpanel_headless/workspace.py` ```` def get_property_history( self, property_name: str, entity_type: str ) -> list[dict[str, Any]]: """Get change history for a property definition. Args: property_name: Name of the property. entity_type: Entity type (e.g. ``"event"``, ``"user"``, ``"group"``). Returns: List of history entries (raw dictionaries) showing changes to the property definition over time. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Property not found (404). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() history = ws.get_property_history("plan_type", "event") for entry in history: print(f"{entry['timestamp']}: {entry['action']}") ``` """ client = self._require_api_client() return client.get_property_history(property_name, entity_type) ```` ### export_lexicon ``` export_lexicon(*, export_types: list[str] | None = None) -> dict[str, Any] ``` Export Lexicon data definitions. Exports event and property definitions from Lexicon, optionally filtered by type. | PARAMETER | DESCRIPTION | | -------------- | --------------------------------------------------------------------------------------------------------------------------- | | `export_types` | Optional list of types to export (e.g. ["All Events and Properties", "All User Profile Properties"]). **TYPE:** \`list[str] | | RETURNS | DESCRIPTION | | ---------------- | ---------------------------------------------------------- | | `dict[str, Any]` | Raw export dictionary containing the exported definitions. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() export = ws.export_lexicon( export_types=["All Events and Properties"] ) print(len(export.get("events", []))) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def export_lexicon( self, *, export_types: list[str] | None = None ) -> dict[str, Any]: """Export Lexicon data definitions. Exports event and property definitions from Lexicon, optionally filtered by type. Args: export_types: Optional list of types to export (e.g. ``["All Events and Properties", "All User Profile Properties"]``). Returns: Raw export dictionary containing the exported definitions. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() export = ws.export_lexicon( export_types=["All Events and Properties"] ) print(len(export.get("events", []))) ``` """ client = self._require_api_client() return client.export_lexicon(export_types=export_types) ```` ### list_drop_filters ``` list_drop_filters() -> list[DropFilter] ``` List all drop filters. | RETURNS | DESCRIPTION | | ------------------ | --------------------------- | | `list[DropFilter]` | List of DropFilter objects. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() filters = ws.list_drop_filters() for f in filters: print(f"{f.event_name}: active={f.active}") ``` Source code in `src/mixpanel_headless/workspace.py` ```` def list_drop_filters(self) -> list[DropFilter]: """List all drop filters. Returns: List of ``DropFilter`` objects. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() filters = ws.list_drop_filters() for f in filters: print(f"{f.event_name}: active={f.active}") ``` """ client = self._require_api_client() raw_list = client.list_drop_filters() return [DropFilter.model_validate(x) for x in raw_list] ```` ### create_drop_filter ``` create_drop_filter(params: CreateDropFilterParams) -> list[DropFilter] ``` Create a new drop filter. | PARAMETER | DESCRIPTION | | --------- | ------------------------------------------------------------------- | | `params` | Drop filter creation parameters. **TYPE:** `CreateDropFilterParams` | | RETURNS | DESCRIPTION | | ------------------ | ----------------------------------------------- | | `list[DropFilter]` | Full list of DropFilter objects after creation. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Validation error (400). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() filters = ws.create_drop_filter( CreateDropFilterParams( event_name="Debug Event", filters={"property": "env", "value": "test"}, ) ) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def create_drop_filter(self, params: CreateDropFilterParams) -> list[DropFilter]: """Create a new drop filter. Args: params: Drop filter creation parameters. Returns: Full list of ``DropFilter`` objects after creation. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Validation error (400). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() filters = ws.create_drop_filter( CreateDropFilterParams( event_name="Debug Event", filters={"property": "env", "value": "test"}, ) ) ``` """ client = self._require_api_client() body = params.model_dump(exclude_none=True) raw_list = client.create_drop_filter(body) return [DropFilter.model_validate(x) for x in raw_list] ```` ### update_drop_filter ``` update_drop_filter(params: UpdateDropFilterParams) -> list[DropFilter] ``` Update a drop filter. | PARAMETER | DESCRIPTION | | --------- | ---------------------------------------------------------------------------------------------- | | `params` | Drop filter update parameters (must include the filter ID). **TYPE:** `UpdateDropFilterParams` | | RETURNS | DESCRIPTION | | ------------------ | --------------------------------------------- | | `list[DropFilter]` | Full list of DropFilter objects after update. | | RAISES | DESCRIPTION | | --------------------- | ------------------------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Filter not found (404) or validation error (400). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() filters = ws.update_drop_filter( UpdateDropFilterParams( id=42, event_name="Debug Event v2" ) ) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def update_drop_filter(self, params: UpdateDropFilterParams) -> list[DropFilter]: """Update a drop filter. Args: params: Drop filter update parameters (must include the filter ID). Returns: Full list of ``DropFilter`` objects after update. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Filter not found (404) or validation error (400). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() filters = ws.update_drop_filter( UpdateDropFilterParams( id=42, event_name="Debug Event v2" ) ) ``` """ client = self._require_api_client() body = params.model_dump(exclude_none=True) raw_list = client.update_drop_filter(body) return [DropFilter.model_validate(x) for x in raw_list] ```` ### delete_drop_filter ``` delete_drop_filter(drop_filter_id: int) -> list[DropFilter] ``` Delete a drop filter. | PARAMETER | DESCRIPTION | | ---------------- | ----------------------------------------- | | `drop_filter_id` | Drop filter ID (integer). **TYPE:** `int` | | RETURNS | DESCRIPTION | | ------------------ | ------------------------------------------ | | `list[DropFilter]` | Full list of remaining DropFilter objects. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Filter not found (404). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() remaining = ws.delete_drop_filter(42) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def delete_drop_filter(self, drop_filter_id: int) -> list[DropFilter]: """Delete a drop filter. Args: drop_filter_id: Drop filter ID (integer). Returns: Full list of remaining ``DropFilter`` objects. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Filter not found (404). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() remaining = ws.delete_drop_filter(42) ``` """ client = self._require_api_client() raw_list = client.delete_drop_filter(drop_filter_id) return [DropFilter.model_validate(x) for x in raw_list] ```` ### get_drop_filter_limits ``` get_drop_filter_limits() -> DropFilterLimitsResponse ``` Get drop filter usage limits. | RETURNS | DESCRIPTION | | -------------------------- | ------------------------------------------------- | | `DropFilterLimitsResponse` | DropFilterLimitsResponse with the maximum allowed | | `DropFilterLimitsResponse` | drop filters for the project. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() limits = ws.get_drop_filter_limits() print(f"Drop filter limit: {limits.filter_limit}") ``` Source code in `src/mixpanel_headless/workspace.py` ```` def get_drop_filter_limits(self) -> DropFilterLimitsResponse: """Get drop filter usage limits. Returns: ``DropFilterLimitsResponse`` with the maximum allowed drop filters for the project. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() limits = ws.get_drop_filter_limits() print(f"Drop filter limit: {limits.filter_limit}") ``` """ client = self._require_api_client() raw = client.get_drop_filter_limits() return DropFilterLimitsResponse.model_validate(raw) ```` ### list_custom_properties ``` list_custom_properties() -> list[CustomProperty] ``` List all custom properties. | RETURNS | DESCRIPTION | | ---------------------- | ------------------------------- | | `list[CustomProperty]` | List of CustomProperty objects. | | RAISES | DESCRIPTION | | --------------------- | ------------------------------------------------------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Server-side data corruption (e.g. invalid displayFormula on a custom property). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() props = ws.list_custom_properties() for p in props: print(f"{p.name}: {p.display_formula}") ``` Source code in `src/mixpanel_headless/workspace.py` ```` def list_custom_properties(self) -> list[CustomProperty]: """List all custom properties. Returns: List of ``CustomProperty`` objects. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Server-side data corruption (e.g. invalid ``displayFormula`` on a custom property). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() props = ws.list_custom_properties() for p in props: print(f"{p.name}: {p.display_formula}") ``` """ client = self._require_api_client() try: raw_list = client.list_custom_properties() except QueryError as exc: # Detect server-side data corruption: the API fails to serialize # when a custom property has an invalid displayFormula. details = exc.details if isinstance(exc.details, dict) else {} body = details.get("response_body", {}) if isinstance(details, dict) else {} if isinstance(body, dict) and body.get("field") == "displayFormula": raise QueryError( "list_custom_properties() failed: the project contains a " "custom property with an invalid displayFormula " "(server-side data corruption). Use " "get_custom_property(id) to retrieve individual " "properties, or contact Mixpanel support.", status_code=exc.status_code, response_body=exc.response_body, request_method=exc.request_method, request_url=exc.request_url, request_params=exc.request_params, ) from exc raise return [CustomProperty.model_validate(x) for x in raw_list] ```` ### create_custom_property ``` create_custom_property(params: CreateCustomPropertyParams) -> CustomProperty ``` Create a new custom property. | PARAMETER | DESCRIPTION | | --------- | ------------------------------------------------------------------------------------------------------------------------------------------- | | `params` | Custom property creation parameters (name, display_formula or behavior, resource_type are required). **TYPE:** `CreateCustomPropertyParams` | | RETURNS | DESCRIPTION | | ---------------- | --------------------------- | | `CustomProperty` | The created CustomProperty. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Validation error (400). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() prop = ws.create_custom_property( CreateCustomPropertyParams( name="Full Name", display_formula='concat(properties["first"], " ", properties["last"])', composed_properties={"first": ComposedPropertyValue(resource_type="event"), "last": ComposedPropertyValue(resource_type="event")}, resource_type="event", ) ) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def create_custom_property( self, params: CreateCustomPropertyParams ) -> CustomProperty: """Create a new custom property. Args: params: Custom property creation parameters (name, display_formula or behavior, resource_type are required). Returns: The created ``CustomProperty``. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Validation error (400). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() prop = ws.create_custom_property( CreateCustomPropertyParams( name="Full Name", display_formula='concat(properties["first"], " ", properties["last"])', composed_properties={"first": ComposedPropertyValue(resource_type="event"), "last": ComposedPropertyValue(resource_type="event")}, resource_type="event", ) ) ``` """ client = self._require_api_client() body = params.model_dump(exclude_none=True, by_alias=True, mode="json") raw = client.create_custom_property(body) return CustomProperty.model_validate(raw) ```` ### get_custom_property ``` get_custom_property(property_id: str) -> CustomProperty ``` Get a custom property by ID. | PARAMETER | DESCRIPTION | | ------------- | -------------------------------------------- | | `property_id` | Custom property ID (string). **TYPE:** `str` | | RETURNS | DESCRIPTION | | ---------------- | -------------------------- | | `CustomProperty` | The CustomProperty object. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Property not found (404). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() prop = ws.get_custom_property("abc123") print(prop.name) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def get_custom_property(self, property_id: str) -> CustomProperty: """Get a custom property by ID. Args: property_id: Custom property ID (string). Returns: The ``CustomProperty`` object. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Property not found (404). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() prop = ws.get_custom_property("abc123") print(prop.name) ``` """ client = self._require_api_client() raw = client.get_custom_property(property_id) return CustomProperty.model_validate(raw) ```` ### update_custom_property ``` update_custom_property( property_id: str, params: UpdateCustomPropertyParams ) -> CustomProperty ``` Update a custom property. | PARAMETER | DESCRIPTION | | ------------- | -------------------------------------------------------- | | `property_id` | Custom property ID (string). **TYPE:** `str` | | `params` | Fields to update. **TYPE:** `UpdateCustomPropertyParams` | | RETURNS | DESCRIPTION | | ---------------- | --------------------------- | | `CustomProperty` | The updated CustomProperty. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Property not found (404) or validation error (400). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() prop = ws.update_custom_property( "abc123", UpdateCustomPropertyParams(name="Renamed Property"), ) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def update_custom_property( self, property_id: str, params: UpdateCustomPropertyParams ) -> CustomProperty: """Update a custom property. Args: property_id: Custom property ID (string). params: Fields to update. Returns: The updated ``CustomProperty``. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Property not found (404) or validation error (400). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() prop = ws.update_custom_property( "abc123", UpdateCustomPropertyParams(name="Renamed Property"), ) ``` """ client = self._require_api_client() body = params.model_dump(exclude_none=True, by_alias=True) raw = client.update_custom_property(property_id, body) return CustomProperty.model_validate(raw) ```` ### delete_custom_property ``` delete_custom_property(property_id: str) -> None ``` Delete a custom property. | PARAMETER | DESCRIPTION | | ------------- | -------------------------------------------- | | `property_id` | Custom property ID (string). **TYPE:** `str` | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Property not found (404). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() ws.delete_custom_property("abc123") ``` Source code in `src/mixpanel_headless/workspace.py` ```` def delete_custom_property(self, property_id: str) -> None: """Delete a custom property. Args: property_id: Custom property ID (string). Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Property not found (404). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() ws.delete_custom_property("abc123") ``` """ client = self._require_api_client() client.delete_custom_property(property_id) ```` ### validate_custom_property ``` validate_custom_property(params: CreateCustomPropertyParams) -> dict[str, Any] ``` Validate a custom property definition without creating it. | PARAMETER | DESCRIPTION | | --------- | ------------------------------------------------------------------------------ | | `params` | Custom property parameters to validate. **TYPE:** `CreateCustomPropertyParams` | | RETURNS | DESCRIPTION | | ---------------- | -------------------------------------- | | `dict[str, Any]` | Validation result as a raw dictionary. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Validation error (400). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() result = ws.validate_custom_property( CreateCustomPropertyParams( name="Full Name", display_formula='concat(properties["first"], " ", properties["last"])', composed_properties={"first": ComposedPropertyValue(resource_type="event"), "last": ComposedPropertyValue(resource_type="event")}, resource_type="event", ) ) print(result) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def validate_custom_property( self, params: CreateCustomPropertyParams ) -> dict[str, Any]: """Validate a custom property definition without creating it. Args: params: Custom property parameters to validate. Returns: Validation result as a raw dictionary. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Validation error (400). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() result = ws.validate_custom_property( CreateCustomPropertyParams( name="Full Name", display_formula='concat(properties["first"], " ", properties["last"])', composed_properties={"first": ComposedPropertyValue(resource_type="event"), "last": ComposedPropertyValue(resource_type="event")}, resource_type="event", ) ) print(result) ``` """ client = self._require_api_client() body = params.model_dump(exclude_none=True, by_alias=True) return client.validate_custom_property(body) ```` ### list_lookup_tables ``` list_lookup_tables(*, data_group_id: int | None = None) -> list[LookupTable] ``` List lookup tables. | PARAMETER | DESCRIPTION | | --------------- | ------------------------------------------------- | | `data_group_id` | Optional filter by data group ID. **TYPE:** \`int | | RETURNS | DESCRIPTION | | ------------------- | ---------------------------- | | `list[LookupTable]` | List of LookupTable objects. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() tables = ws.list_lookup_tables() for t in tables: print(f"{t.name} (mapped={t.has_mapped_properties})") ``` Source code in `src/mixpanel_headless/workspace.py` ```` def list_lookup_tables( self, *, data_group_id: int | None = None ) -> list[LookupTable]: """List lookup tables. Args: data_group_id: Optional filter by data group ID. Returns: List of ``LookupTable`` objects. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() tables = ws.list_lookup_tables() for t in tables: print(f"{t.name} (mapped={t.has_mapped_properties})") ``` """ client = self._require_api_client() raw_list = client.list_lookup_tables(data_group_id=data_group_id) return [LookupTable.model_validate(x) for x in raw_list] ```` ### upload_lookup_table ``` upload_lookup_table( params: UploadLookupTableParams, *, poll_interval: float = 2.0, max_poll_seconds: float = 300.0, ) -> LookupTable ``` Upload a CSV file as a new lookup table. Performs a 3-step upload process: 1. Obtains a signed upload URL from the API. 1. Uploads the CSV file to the signed URL. 1. Registers the lookup table with the uploaded data. For files >= 5 MB, the API processes the upload asynchronously. This method automatically polls until processing completes. | PARAMETER | DESCRIPTION | | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------- | | `params` | Upload parameters including name, file_path (path to the CSV file), and optional data_group_id. **TYPE:** `UploadLookupTableParams` | | `poll_interval` | Seconds between status polls for async uploads. **TYPE:** `float` **DEFAULT:** `2.0` | | `max_poll_seconds` | Maximum seconds to wait for async processing. **TYPE:** `float` **DEFAULT:** `300.0` | | RETURNS | DESCRIPTION | | ------------- | ------------------------------- | | `LookupTable` | The created LookupTable object. | | RAISES | DESCRIPTION | | ----------------------- | ----------------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Validation error (400) or file not found. | | `ServerError` | Server-side errors (5xx). | | `FileNotFoundError` | If the CSV file does not exist. | | `MixpanelHeadlessError` | Async processing timed out or failed. | Example ``` ws = Workspace() table = ws.upload_lookup_table( UploadLookupTableParams( name="Country Codes", file_path="/path/to/countries.csv", ) ) print(f"Created: {table.name}") ``` Source code in `src/mixpanel_headless/workspace.py` ```` def upload_lookup_table( self, params: UploadLookupTableParams, *, poll_interval: float = 2.0, max_poll_seconds: float = 300.0, ) -> LookupTable: """Upload a CSV file as a new lookup table. Performs a 3-step upload process: 1. Obtains a signed upload URL from the API. 2. Uploads the CSV file to the signed URL. 3. Registers the lookup table with the uploaded data. For files >= 5 MB, the API processes the upload asynchronously. This method automatically polls until processing completes. Args: params: Upload parameters including ``name``, ``file_path`` (path to the CSV file), and optional ``data_group_id``. poll_interval: Seconds between status polls for async uploads. max_poll_seconds: Maximum seconds to wait for async processing. Returns: The created ``LookupTable`` object. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Validation error (400) or file not found. ServerError: Server-side errors (5xx). FileNotFoundError: If the CSV file does not exist. MixpanelHeadlessError: Async processing timed out or failed. Example: ```python ws = Workspace() table = ws.upload_lookup_table( UploadLookupTableParams( name="Country Codes", file_path="/path/to/countries.csv", ) ) print(f"Created: {table.name}") ``` """ client = self._require_api_client() logger = logging.getLogger(__name__) # Step 1: Get signed upload URL url_info = client.get_lookup_upload_url() # Step 2: Read and upload the CSV file csv_bytes = Path(params.file_path).read_bytes() client.upload_to_signed_url(url_info["url"], csv_bytes) # Step 3: Register the lookup table form_data: dict[str, str] = { "name": params.name, "path": url_info["path"], "key": url_info["key"], } if params.data_group_id is not None: form_data["data-group-id"] = str(params.data_group_id) raw = client.register_lookup_table(form_data) # The API returns {"uploadId": "..."} for files >= 5 MB, # indicating async processing via Celery. upload_id = raw.get("uploadId") if isinstance(raw, dict) else None if upload_id is not None: logger.info( "Lookup table upload is processing asynchronously " "(uploadId=%s), polling for completion...", upload_id, ) raw = self._poll_lookup_upload( client, upload_id, poll_interval, max_poll_seconds ) # Upload response may only contain {'id': '...'} without 'name'; # inject the name from params so LookupTable validation succeeds. if isinstance(raw, dict) and "name" not in raw: raw = {**raw, "name": params.name} return LookupTable.model_validate(raw) ```` ### mark_lookup_table_ready ``` mark_lookup_table_ready(params: MarkLookupTableReadyParams) -> LookupTable ``` Mark a lookup table as ready after upload. | PARAMETER | DESCRIPTION | | --------- | -------------------------------------------------------------------------------------------------- | | `params` | Parameters including name, key, and optional data_group_id. **TYPE:** `MarkLookupTableReadyParams` | | RETURNS | DESCRIPTION | | ------------- | ------------------------ | | `LookupTable` | The updated LookupTable. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Validation error (400). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() table = ws.mark_lookup_table_ready( MarkLookupTableReadyParams( name="Country Codes", key="uploads/abc123.csv", ) ) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def mark_lookup_table_ready( self, params: MarkLookupTableReadyParams ) -> LookupTable: """Mark a lookup table as ready after upload. Args: params: Parameters including ``name``, ``key``, and optional ``data_group_id``. Returns: The updated ``LookupTable``. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Validation error (400). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() table = ws.mark_lookup_table_ready( MarkLookupTableReadyParams( name="Country Codes", key="uploads/abc123.csv", ) ) ``` """ client = self._require_api_client() form_data: dict[str, str] = { "name": params.name, "key": params.key, } if params.data_group_id is not None: form_data["data-group-id"] = str(params.data_group_id) raw = client.mark_lookup_table_ready(form_data) return LookupTable.model_validate(raw) ```` ### get_lookup_upload_url ``` get_lookup_upload_url(content_type: str = 'text/csv') -> LookupTableUploadUrl ``` Get a signed URL for uploading lookup table data. | PARAMETER | DESCRIPTION | | -------------- | ------------------------------------------------------------------------------------------------ | | `content_type` | MIME type of the file to upload (default: "text/csv"). **TYPE:** `str` **DEFAULT:** `'text/csv'` | | RETURNS | DESCRIPTION | | ---------------------- | -------------------------------------------------------- | | `LookupTableUploadUrl` | LookupTableUploadUrl with the signed URL, path, and key. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() url_info = ws.get_lookup_upload_url() print(url_info.url) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def get_lookup_upload_url( self, content_type: str = "text/csv" ) -> LookupTableUploadUrl: """Get a signed URL for uploading lookup table data. Args: content_type: MIME type of the file to upload (default: ``"text/csv"``). Returns: ``LookupTableUploadUrl`` with the signed URL, path, and key. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() url_info = ws.get_lookup_upload_url() print(url_info.url) ``` """ client = self._require_api_client() raw = client.get_lookup_upload_url(content_type) return LookupTableUploadUrl.model_validate(raw) ```` ### get_lookup_upload_status ``` get_lookup_upload_status(upload_id: str) -> dict[str, Any] ``` Get the processing status of a lookup table upload. | PARAMETER | DESCRIPTION | | ----------- | ----------------------------------------------------------- | | `upload_id` | Upload ID returned from the upload process. **TYPE:** `str` | | RETURNS | DESCRIPTION | | ---------------- | ---------------------------------------------- | | `dict[str, Any]` | Raw status dictionary with processing details. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Upload not found (404). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() status = ws.get_lookup_upload_status("upload-abc123") print(status["state"]) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def get_lookup_upload_status(self, upload_id: str) -> dict[str, Any]: """Get the processing status of a lookup table upload. Args: upload_id: Upload ID returned from the upload process. Returns: Raw status dictionary with processing details. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Upload not found (404). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() status = ws.get_lookup_upload_status("upload-abc123") print(status["state"]) ``` """ client = self._require_api_client() return client.get_lookup_upload_status(upload_id) ```` ### update_lookup_table ``` update_lookup_table( data_group_id: int, params: UpdateLookupTableParams ) -> LookupTable ``` Update a lookup table. | PARAMETER | DESCRIPTION | | --------------- | ----------------------------------------------------- | | `data_group_id` | Data group ID of the lookup table. **TYPE:** `int` | | `params` | Fields to update. **TYPE:** `UpdateLookupTableParams` | | RETURNS | DESCRIPTION | | ------------- | ------------------------ | | `LookupTable` | The updated LookupTable. | | RAISES | DESCRIPTION | | --------------------- | ------------------------------------------------ | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Table not found (404) or validation error (400). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() table = ws.update_lookup_table( 123, UpdateLookupTableParams(name="Renamed Table"), ) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def update_lookup_table( self, data_group_id: int, params: UpdateLookupTableParams ) -> LookupTable: """Update a lookup table. Args: data_group_id: Data group ID of the lookup table. params: Fields to update. Returns: The updated ``LookupTable``. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Table not found (404) or validation error (400). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() table = ws.update_lookup_table( 123, UpdateLookupTableParams(name="Renamed Table"), ) ``` """ client = self._require_api_client() body = params.model_dump(exclude_none=True) raw = client.update_lookup_table(data_group_id, body) return LookupTable.model_validate(raw) ```` ### delete_lookup_tables ``` delete_lookup_tables(data_group_ids: list[int]) -> None ``` Delete one or more lookup tables. | PARAMETER | DESCRIPTION | | ---------------- | ------------------------------------------------------- | | `data_group_ids` | List of data group IDs to delete. **TYPE:** `list[int]` | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Validation error (400). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() ws.delete_lookup_tables([123, 456]) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def delete_lookup_tables(self, data_group_ids: list[int]) -> None: """Delete one or more lookup tables. Args: data_group_ids: List of data group IDs to delete. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Validation error (400). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() ws.delete_lookup_tables([123, 456]) ``` """ client = self._require_api_client() client.delete_lookup_tables(data_group_ids) ```` ### download_lookup_table ``` download_lookup_table( data_group_id: int, *, file_name: str | None = None, limit: int | None = None, ) -> bytes ``` Download lookup table data as raw bytes (CSV). | PARAMETER | DESCRIPTION | | --------------- | -------------------------------------------------- | | `data_group_id` | Data group ID of the lookup table. **TYPE:** `int` | | `file_name` | Optional file name filter. **TYPE:** \`str | | `limit` | Optional row limit. **TYPE:** \`int | | RETURNS | DESCRIPTION | | ------- | --------------------------------------- | | `bytes` | Raw CSV bytes of the lookup table data. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Table not found (404). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() csv_data = ws.download_lookup_table(123) Path("output.csv").write_bytes(csv_data) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def download_lookup_table( self, data_group_id: int, *, file_name: str | None = None, limit: int | None = None, ) -> bytes: """Download lookup table data as raw bytes (CSV). Args: data_group_id: Data group ID of the lookup table. file_name: Optional file name filter. limit: Optional row limit. Returns: Raw CSV bytes of the lookup table data. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Table not found (404). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() csv_data = ws.download_lookup_table(123) Path("output.csv").write_bytes(csv_data) ``` """ client = self._require_api_client() return client.download_lookup_table( data_group_id, file_name=file_name, limit=limit ) ```` ### get_lookup_download_url ``` get_lookup_download_url(data_group_id: int) -> str ``` Get a signed download URL for a lookup table. | PARAMETER | DESCRIPTION | | --------------- | -------------------------------------------------- | | `data_group_id` | Data group ID of the lookup table. **TYPE:** `int` | | RETURNS | DESCRIPTION | | ------- | -------------------------------------------------------- | | `str` | Signed URL string for downloading the lookup table data. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Table not found (404). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() url = ws.get_lookup_download_url(123) print(url) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def get_lookup_download_url(self, data_group_id: int) -> str: """Get a signed download URL for a lookup table. Args: data_group_id: Data group ID of the lookup table. Returns: Signed URL string for downloading the lookup table data. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Table not found (404). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() url = ws.get_lookup_download_url(123) print(url) ``` """ client = self._require_api_client() return client.get_lookup_download_url(data_group_id) ```` ### list_custom_events ``` list_custom_events() -> list[EventDefinition] ``` List all custom events. | RETURNS | DESCRIPTION | | ----------------------- | -------------------------------------------------- | | `list[EventDefinition]` | List of EventDefinition objects for custom events. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() events = ws.list_custom_events() for e in events: print(e.name) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def list_custom_events(self) -> list[EventDefinition]: """List all custom events. Returns: List of ``EventDefinition`` objects for custom events. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() events = ws.list_custom_events() for e in events: print(e.name) ``` """ client = self._require_api_client() raw_list = client.list_custom_events() return [EventDefinition.model_validate(x) for x in raw_list] ```` ### update_custom_event ``` update_custom_event( custom_event_id: int, params: UpdateEventDefinitionParams ) -> EventDefinition ``` Update a custom event's lexicon entry (description, tags, etc.). The Mixpanel `data-definitions/events/` endpoint matches updates by the most specific identifier; for custom events that's the `customEventId`. This SDK method requires the id (rather than the display name) to avoid creating orphan lexicon entries β€” passing a name alone causes the server to fabricate a new, unlinked entry. Get the id from :meth:`create_custom_event`'s return value (`CustomEvent.id`) or from the `custom_event_id` field on entries returned by :meth:`list_custom_events`. | PARAMETER | DESCRIPTION | | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------- | | `custom_event_id` | Server-assigned custom event ID. **TYPE:** `int` | | `params` | Fields to update. See :class:UpdateEventDefinitionParams for the full list of supported fields. **TYPE:** `UpdateEventDefinitionParams` | | RETURNS | DESCRIPTION | | ----------------- | ------------------------------------------------------- | | `EventDefinition` | The updated EventDefinition (lexicon view of the custom | | `EventDefinition` | event, with custom_event_id populated). | | RAISES | DESCRIPTION | | ----------------------- | ------------------------------------------------------------------------------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Event not found (404) or validation error (400). | | `ServerError` | Server-side errors (5xx). | | `MixpanelHeadlessError` | Server returned an entry with a different customEventId than requested (code="UPDATE_TARGET_MISMATCH"). | Example ``` ws = Workspace() ce = ws.create_custom_event(CreateCustomEventParams( name="Metric Tree Opened", alternatives=["Enter room"], )) event = ws.update_custom_event( ce.id, UpdateEventDefinitionParams( description="Fires when a user opens a metric tree canvas.", verified=True, ), ) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def update_custom_event( self, custom_event_id: int, params: UpdateEventDefinitionParams ) -> EventDefinition: """Update a custom event's lexicon entry (description, tags, etc.). The Mixpanel ``data-definitions/events/`` endpoint matches updates by the most specific identifier; for custom events that's the ``customEventId``. This SDK method requires the id (rather than the display name) to avoid creating orphan lexicon entries β€” passing a name alone causes the server to fabricate a new, unlinked entry. Get the id from :meth:`create_custom_event`'s return value (``CustomEvent.id``) or from the ``custom_event_id`` field on entries returned by :meth:`list_custom_events`. Args: custom_event_id: Server-assigned custom event ID. params: Fields to update. See :class:`UpdateEventDefinitionParams` for the full list of supported fields. Returns: The updated ``EventDefinition`` (lexicon view of the custom event, with ``custom_event_id`` populated). Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Event not found (404) or validation error (400). ServerError: Server-side errors (5xx). MixpanelHeadlessError: Server returned an entry with a different ``customEventId`` than requested (``code="UPDATE_TARGET_MISMATCH"``). Example: ```python ws = Workspace() ce = ws.create_custom_event(CreateCustomEventParams( name="Metric Tree Opened", alternatives=["Enter room"], )) event = ws.update_custom_event( ce.id, UpdateEventDefinitionParams( description="Fires when a user opens a metric tree canvas.", verified=True, ), ) ``` """ client = self._require_api_client() body = params.model_dump(exclude_none=True) raw = client.update_custom_event(custom_event_id, body) return EventDefinition.model_validate(raw) ```` ### delete_custom_event ``` delete_custom_event(custom_event_id: int) -> None ``` Delete a custom event. Identifies the entry by `custom_event_id` (not name) for the same reason :meth:`update_custom_event` does: a name-only DELETE against the data-definitions endpoint is ambiguous when multiple entries share a display name and may silently delete the wrong row, an auto-derived orphan lexicon entry, or no-op while still reporting success. Get the id from :meth:`create_custom_event`'s return value (`CustomEvent.id`) or from the `custom_event_id` field on entries returned by :meth:`list_custom_events`. | PARAMETER | DESCRIPTION | | ----------------- | ------------------------------------------------ | | `custom_event_id` | Server-assigned custom event ID. **TYPE:** `int` | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Event not found (404). | | `ServerError` | Server-side errors (5xx). | Example ``` ws = Workspace() ws.delete_custom_event(42) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def delete_custom_event(self, custom_event_id: int) -> None: """Delete a custom event. Identifies the entry by ``custom_event_id`` (not name) for the same reason :meth:`update_custom_event` does: a name-only DELETE against the data-definitions endpoint is ambiguous when multiple entries share a display name and may silently delete the wrong row, an auto-derived orphan lexicon entry, or no-op while still reporting success. Get the id from :meth:`create_custom_event`'s return value (``CustomEvent.id``) or from the ``custom_event_id`` field on entries returned by :meth:`list_custom_events`. Args: custom_event_id: Server-assigned custom event ID. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Event not found (404). ServerError: Server-side errors (5xx). Example: ```python ws = Workspace() ws.delete_custom_event(42) ``` """ client = self._require_api_client() client.delete_custom_event(custom_event_id) ```` ### list_schema_registry ``` list_schema_registry(*, entity_type: str | None = None) -> list[SchemaEntry] ``` List schema registry entries. | PARAMETER | DESCRIPTION | | ------------- | --------------------------------------------------------------------------------------------------------- | | `entity_type` | Filter by entity type ("event", "custom_event", "profile"). If None, returns all schemas. **TYPE:** \`str | | RETURNS | DESCRIPTION | | ------------------- | ---------------------------- | | `list[SchemaEntry]` | List of SchemaEntry objects. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `RateLimitError` | Rate limit exceeded (429). | Example ``` ws = Workspace() schemas = ws.list_schema_registry(entity_type="event") for s in schemas: print(f"{s.name}: {s.entity_type}") ``` Source code in `src/mixpanel_headless/workspace.py` ```` def list_schema_registry( self, *, entity_type: str | None = None, ) -> list[SchemaEntry]: """List schema registry entries. Args: entity_type: Filter by entity type ("event", "custom_event", "profile"). If None, returns all schemas. Returns: List of ``SchemaEntry`` objects. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). RateLimitError: Rate limit exceeded (429). Example: ```python ws = Workspace() schemas = ws.list_schema_registry(entity_type="event") for s in schemas: print(f"{s.name}: {s.entity_type}") ``` """ client = self._require_api_client() raw_list = client.list_schema_registry(entity_type=entity_type) return [SchemaEntry.model_validate(r) for r in raw_list] ```` ### create_schema ``` create_schema( entity_type: str, entity_name: str, schema_json: dict[str, Any] ) -> dict[str, Any] ``` Create a single schema definition. | PARAMETER | DESCRIPTION | | ------------- | ----------------------------------------------------------------- | | `entity_type` | Entity type ("event", "custom_event", "profile"). **TYPE:** `str` | | `entity_name` | Entity name (event name or "$user" for profile). **TYPE:** `str` | | `schema_json` | JSON Schema Draft 7 definition. **TYPE:** `dict[str, Any]` | | RETURNS | DESCRIPTION | | ---------------- | ----------------------- | | `dict[str, Any]` | Created schema as dict. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Validation error (400). | | `RateLimitError` | Rate limit exceeded (429). | Example ``` ws = Workspace() ws.create_schema("event", "Purchase", { "properties": {"amount": {"type": "number"}} }) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def create_schema( self, entity_type: str, entity_name: str, schema_json: dict[str, Any], ) -> dict[str, Any]: """Create a single schema definition. Args: entity_type: Entity type ("event", "custom_event", "profile"). entity_name: Entity name (event name or "$user" for profile). schema_json: JSON Schema Draft 7 definition. Returns: Created schema as dict. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Validation error (400). RateLimitError: Rate limit exceeded (429). Example: ```python ws = Workspace() ws.create_schema("event", "Purchase", { "properties": {"amount": {"type": "number"}} }) ``` """ client = self._require_api_client() return client.create_schema(entity_type, entity_name, schema_json) ```` ### create_schemas_bulk ``` create_schemas_bulk( params: BulkCreateSchemasParams, ) -> BulkCreateSchemasResponse ``` Bulk create schemas. | PARAMETER | DESCRIPTION | | --------- | ---------------------------------------------------------------------------------------------------------- | | `params` | Bulk creation parameters with entries list and optional truncate flag. **TYPE:** `BulkCreateSchemasParams` | | RETURNS | DESCRIPTION | | --------------------------- | --------------------------------------- | | `BulkCreateSchemasResponse` | Response with added and deleted counts. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Validation error (400). | | `RateLimitError` | Rate limit exceeded (429). | Example ``` ws = Workspace() result = ws.create_schemas_bulk(BulkCreateSchemasParams( entries=[SchemaEntry(...)], truncate=True )) print(f"Added: {result.added}") ``` Source code in `src/mixpanel_headless/workspace.py` ```` def create_schemas_bulk( self, params: BulkCreateSchemasParams, ) -> BulkCreateSchemasResponse: """Bulk create schemas. Args: params: Bulk creation parameters with entries list and optional truncate flag. Returns: Response with ``added`` and ``deleted`` counts. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Validation error (400). RateLimitError: Rate limit exceeded (429). Example: ```python ws = Workspace() result = ws.create_schemas_bulk(BulkCreateSchemasParams( entries=[SchemaEntry(...)], truncate=True )) print(f"Added: {result.added}") ``` """ client = self._require_api_client() raw = client.create_schemas_bulk( params.model_dump(exclude_none=True, by_alias=True) ) return BulkCreateSchemasResponse.model_validate(raw) ```` ### update_schema ``` update_schema( entity_type: str, entity_name: str, schema_json: dict[str, Any] ) -> dict[str, Any] ``` Update a single schema definition (merge semantics). | PARAMETER | DESCRIPTION | | ------------- | ---------------------------------------------------------------------- | | `entity_type` | Entity type. **TYPE:** `str` | | `entity_name` | Entity name. **TYPE:** `str` | | `schema_json` | Partial JSON Schema to merge with existing. **TYPE:** `dict[str, Any]` | | RETURNS | DESCRIPTION | | ---------------- | ----------------------- | | `dict[str, Any]` | Updated schema as dict. | | RAISES | DESCRIPTION | | --------------------- | ------------------------------------------------ | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Entity not found or validation error (400, 404). | | `RateLimitError` | Rate limit exceeded (429). | Example ``` ws = Workspace() ws.update_schema("event", "Purchase", { "properties": {"tax": {"type": "number"}} }) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def update_schema( self, entity_type: str, entity_name: str, schema_json: dict[str, Any], ) -> dict[str, Any]: """Update a single schema definition (merge semantics). Args: entity_type: Entity type. entity_name: Entity name. schema_json: Partial JSON Schema to merge with existing. Returns: Updated schema as dict. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Entity not found or validation error (400, 404). RateLimitError: Rate limit exceeded (429). Example: ```python ws = Workspace() ws.update_schema("event", "Purchase", { "properties": {"tax": {"type": "number"}} }) ``` """ client = self._require_api_client() return client.update_schema(entity_type, entity_name, schema_json) ```` ### update_schemas_bulk ``` update_schemas_bulk(params: BulkCreateSchemasParams) -> list[BulkPatchResult] ``` Bulk update schemas (merge semantics per entry). | PARAMETER | DESCRIPTION | | --------- | ----------------------------------------------------------------------------- | | `params` | Bulk update parameters with entries list. **TYPE:** `BulkCreateSchemasParams` | | RETURNS | DESCRIPTION | | ----------------------- | -------------------------------------------------------- | | `list[BulkPatchResult]` | List of per-entry results with status ("ok" or "error"). | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `RateLimitError` | Rate limit exceeded (429). | Example ``` ws = Workspace() results = ws.update_schemas_bulk(BulkCreateSchemasParams( entries=[SchemaEntry(...)] )) for r in results: print(f"{r.name}: {r.status}") ``` Source code in `src/mixpanel_headless/workspace.py` ```` def update_schemas_bulk( self, params: BulkCreateSchemasParams, ) -> list[BulkPatchResult]: """Bulk update schemas (merge semantics per entry). Args: params: Bulk update parameters with entries list. Returns: List of per-entry results with status ("ok" or "error"). Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). RateLimitError: Rate limit exceeded (429). Example: ```python ws = Workspace() results = ws.update_schemas_bulk(BulkCreateSchemasParams( entries=[SchemaEntry(...)] )) for r in results: print(f"{r.name}: {r.status}") ``` """ client = self._require_api_client() raw_list = client.update_schemas_bulk( params.model_dump(exclude_none=True, by_alias=True) ) return [BulkPatchResult.model_validate(r) for r in raw_list] ```` ### delete_schemas ``` delete_schemas( *, entity_type: str | None = None, entity_name: str | None = None ) -> DeleteSchemasResponse ``` Delete schemas by entity type and/or name. If both provided, deletes a single schema. If only entity_type, deletes all schemas of that type. If neither, deletes all schemas. | PARAMETER | DESCRIPTION | | ------------- | ------------------------------------------------------------- | | `entity_type` | Filter by entity type. **TYPE:** \`str | | `entity_name` | Filter by entity name (requires entity_type). **TYPE:** \`str | | RETURNS | DESCRIPTION | | ----------------------- | --------------------------- | | `DeleteSchemasResponse` | Response with delete_count. | | RAISES | DESCRIPTION | | ----------------------- | ----------------------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Invalid parameters (400). | | `RateLimitError` | Rate limit exceeded (429). | | `MixpanelHeadlessError` | If entity_name is provided without entity_type. | Example ``` ws = Workspace() resp = ws.delete_schemas(entity_type="event", entity_name="Purchase") print(f"Deleted: {resp.delete_count}") ``` Source code in `src/mixpanel_headless/workspace.py` ```` def delete_schemas( self, *, entity_type: str | None = None, entity_name: str | None = None, ) -> DeleteSchemasResponse: """Delete schemas by entity type and/or name. If both provided, deletes a single schema. If only entity_type, deletes all schemas of that type. If neither, deletes all schemas. Args: entity_type: Filter by entity type. entity_name: Filter by entity name (requires entity_type). Returns: Response with ``delete_count``. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Invalid parameters (400). RateLimitError: Rate limit exceeded (429). MixpanelHeadlessError: If entity_name is provided without entity_type. Example: ```python ws = Workspace() resp = ws.delete_schemas(entity_type="event", entity_name="Purchase") print(f"Deleted: {resp.delete_count}") ``` """ if entity_name is not None and entity_type is None: raise MixpanelHeadlessError( "entity_name requires entity_type: providing entity_name " "without entity_type would delete all schemas", ) client = self._require_api_client() raw = client.delete_schemas(entity_type=entity_type, entity_name=entity_name) return DeleteSchemasResponse.model_validate(raw) ```` ### get_schema_enforcement ``` get_schema_enforcement(*, fields: str | None = None) -> SchemaEnforcementConfig ``` Get current schema enforcement configuration. | PARAMETER | DESCRIPTION | | --------- | ------------------------------------------------------------------------------------------------------------- | | `fields` | Comma-separated field names to return (e.g., "ruleEvent,state"). If None, returns all fields. **TYPE:** \`str | | RETURNS | DESCRIPTION | | ------------------------- | --------------------------------- | | `SchemaEnforcementConfig` | Schema enforcement configuration. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | No enforcement configured (404). | Example ``` ws = Workspace() config = ws.get_schema_enforcement() print(f"Rule: {config.rule_event}") ``` Source code in `src/mixpanel_headless/workspace.py` ```` def get_schema_enforcement( self, *, fields: str | None = None, ) -> SchemaEnforcementConfig: """Get current schema enforcement configuration. Args: fields: Comma-separated field names to return (e.g., "ruleEvent,state"). If None, returns all fields. Returns: Schema enforcement configuration. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: No enforcement configured (404). Example: ```python ws = Workspace() config = ws.get_schema_enforcement() print(f"Rule: {config.rule_event}") ``` """ client = self._require_api_client() raw = client.get_schema_enforcement(fields=fields) return SchemaEnforcementConfig.model_validate(raw) ```` ### init_schema_enforcement ``` init_schema_enforcement(params: InitSchemaEnforcementParams) -> dict[str, Any] ``` Initialize schema enforcement. | PARAMETER | DESCRIPTION | | --------- | ------------------------------------------------------------------------ | | `params` | Init parameters with rule_event. **TYPE:** `InitSchemaEnforcementParams` | | RETURNS | DESCRIPTION | | ---------------- | ------------------------- | | `dict[str, Any]` | Raw API response as dict. | | RAISES | DESCRIPTION | | --------------------- | ------------------------------------------------ | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Already initialized or invalid rule_event (400). | Example ``` ws = Workspace() ws.init_schema_enforcement( InitSchemaEnforcementParams(rule_event="Warn and Accept") ) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def init_schema_enforcement( self, params: InitSchemaEnforcementParams, ) -> dict[str, Any]: """Initialize schema enforcement. Args: params: Init parameters with rule_event. Returns: Raw API response as dict. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Already initialized or invalid rule_event (400). Example: ```python ws = Workspace() ws.init_schema_enforcement( InitSchemaEnforcementParams(rule_event="Warn and Accept") ) ``` """ client = self._require_api_client() return client.init_schema_enforcement( params.model_dump(exclude_none=True, by_alias=True) ) ```` ### update_schema_enforcement ``` update_schema_enforcement( params: UpdateSchemaEnforcementParams, ) -> dict[str, Any] ``` Partially update enforcement configuration. | PARAMETER | DESCRIPTION | | --------- | -------------------------------------------------------------------- | | `params` | Partial update parameters. **TYPE:** `UpdateSchemaEnforcementParams` | | RETURNS | DESCRIPTION | | ---------------- | ------------------------- | | `dict[str, Any]` | Raw API response as dict. | | RAISES | DESCRIPTION | | --------------------- | ---------------------------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | No enforcement configured or validation error (400). | Example ``` ws = Workspace() ws.update_schema_enforcement( UpdateSchemaEnforcementParams(rule_event="Warn and Drop") ) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def update_schema_enforcement( self, params: UpdateSchemaEnforcementParams, ) -> dict[str, Any]: """Partially update enforcement configuration. Args: params: Partial update parameters. Returns: Raw API response as dict. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: No enforcement configured or validation error (400). Example: ```python ws = Workspace() ws.update_schema_enforcement( UpdateSchemaEnforcementParams(rule_event="Warn and Drop") ) ``` """ client = self._require_api_client() return client.update_schema_enforcement( params.model_dump(exclude_none=True, by_alias=True) ) ```` ### replace_schema_enforcement ``` replace_schema_enforcement( params: ReplaceSchemaEnforcementParams, ) -> dict[str, Any] ``` Fully replace enforcement configuration. | PARAMETER | DESCRIPTION | | --------- | --------------------------------------------------------------------------- | | `params` | Complete replacement parameters. **TYPE:** `ReplaceSchemaEnforcementParams` | | RETURNS | DESCRIPTION | | ---------------- | ------------------------- | | `dict[str, Any]` | Raw API response as dict. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Validation error (400). | Example ``` ws = Workspace() ws.replace_schema_enforcement(ReplaceSchemaEnforcementParams( events=[...], common_properties=[...], user_properties=[...], rule_event="Warn and Hide", notification_emails=["admin@example.com"], )) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def replace_schema_enforcement( self, params: ReplaceSchemaEnforcementParams, ) -> dict[str, Any]: """Fully replace enforcement configuration. Args: params: Complete replacement parameters. Returns: Raw API response as dict. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Validation error (400). Example: ```python ws = Workspace() ws.replace_schema_enforcement(ReplaceSchemaEnforcementParams( events=[...], common_properties=[...], user_properties=[...], rule_event="Warn and Hide", notification_emails=["admin@example.com"], )) ``` """ client = self._require_api_client() return client.replace_schema_enforcement( params.model_dump(exclude_none=True, by_alias=True) ) ```` ### delete_schema_enforcement ``` delete_schema_enforcement() -> dict[str, Any] ``` Delete enforcement configuration. | RETURNS | DESCRIPTION | | ---------------- | ------------------------- | | `dict[str, Any]` | Raw API response as dict. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | No enforcement configured (404). | Example ``` ws = Workspace() ws.delete_schema_enforcement() ``` Source code in `src/mixpanel_headless/workspace.py` ```` def delete_schema_enforcement(self) -> dict[str, Any]: """Delete enforcement configuration. Returns: Raw API response as dict. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: No enforcement configured (404). Example: ```python ws = Workspace() ws.delete_schema_enforcement() ``` """ client = self._require_api_client() return client.delete_schema_enforcement() ```` ### run_audit ``` run_audit() -> AuditResponse ``` Run a full data audit (events + properties). | RETURNS | DESCRIPTION | | --------------- | --------------------------------------------------------- | | `AuditResponse` | Audit response with violations and computed_at timestamp. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | No schemas defined (400). | Example ``` ws = Workspace() audit = ws.run_audit() for v in audit.violations: print(f"{v.violation}: {v.name} ({v.count})") ``` Source code in `src/mixpanel_headless/workspace.py` ```` def run_audit(self) -> AuditResponse: """Run a full data audit (events + properties). Returns: Audit response with violations and ``computed_at`` timestamp. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: No schemas defined (400). Example: ```python ws = Workspace() audit = ws.run_audit() for v in audit.violations: print(f"{v.violation}: {v.name} ({v.count})") ``` """ client = self._require_api_client() raw = client.run_audit() # raw is [violations_list, {"computed_at": ...}] if not raw: return AuditResponse(violations=[], computed_at="") if not isinstance(raw[0], list): raise MixpanelHeadlessError( f"Unexpected audit response: expected list of violations, " f"got {type(raw[0]).__name__}", ) violations = [AuditViolation.model_validate(v) for v in raw[0]] metadata = raw[1] if len(raw) > 1 and isinstance(raw[1], dict) else {} return AuditResponse( violations=violations, computed_at=metadata.get("computed_at", ""), ) ```` ### run_audit_events_only ``` run_audit_events_only() -> AuditResponse ``` Run an events-only data audit (faster). | RETURNS | DESCRIPTION | | --------------- | ------------------------------------------ | | `AuditResponse` | Audit response with event violations only. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | No schemas defined (400). | Example ``` ws = Workspace() audit = ws.run_audit_events_only() ``` Source code in `src/mixpanel_headless/workspace.py` ```` def run_audit_events_only(self) -> AuditResponse: """Run an events-only data audit (faster). Returns: Audit response with event violations only. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: No schemas defined (400). Example: ```python ws = Workspace() audit = ws.run_audit_events_only() ``` """ client = self._require_api_client() raw = client.run_audit_events_only() if not raw: return AuditResponse(violations=[], computed_at="") if not isinstance(raw[0], list): raise MixpanelHeadlessError( f"Unexpected audit response: expected list of violations, " f"got {type(raw[0]).__name__}", ) violations = [AuditViolation.model_validate(v) for v in raw[0]] metadata = raw[1] if len(raw) > 1 and isinstance(raw[1], dict) else {} return AuditResponse( violations=violations, computed_at=metadata.get("computed_at", ""), ) ```` ### list_data_volume_anomalies ``` list_data_volume_anomalies( *, query_params: dict[str, str] | None = None ) -> list[DataVolumeAnomaly] ``` List detected data volume anomalies. | PARAMETER | DESCRIPTION | | -------------- | ---------------------------------------------------------------------------- | | `query_params` | Optional filters (status, limit, event_id, etc.). **TYPE:** \`dict[str, str] | | RETURNS | DESCRIPTION | | ------------------------- | ---------------------------------- | | `list[DataVolumeAnomaly]` | List of DataVolumeAnomaly objects. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | Example ``` ws = Workspace() anomalies = ws.list_data_volume_anomalies( query_params={"status": "open"} ) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def list_data_volume_anomalies( self, *, query_params: dict[str, str] | None = None, ) -> list[DataVolumeAnomaly]: """List detected data volume anomalies. Args: query_params: Optional filters (status, limit, event_id, etc.). Returns: List of ``DataVolumeAnomaly`` objects. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). Example: ```python ws = Workspace() anomalies = ws.list_data_volume_anomalies( query_params={"status": "open"} ) ``` """ client = self._require_api_client() raw_list = client.list_data_volume_anomalies(query_params=query_params) return [DataVolumeAnomaly.model_validate(r) for r in raw_list] ```` ### update_anomaly ``` update_anomaly(params: UpdateAnomalyParams) -> dict[str, Any] ``` Update the status of a single anomaly. | PARAMETER | DESCRIPTION | | --------- | ------------------------------------------------------------------------------------- | | `params` | Update parameters with id, status, and anomaly_class. **TYPE:** `UpdateAnomalyParams` | | RETURNS | DESCRIPTION | | ---------------- | ------------------------- | | `dict[str, Any]` | Raw API response as dict. | | RAISES | DESCRIPTION | | --------------------- | ---------------------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Anomaly not found or invalid parameters (400). | Example ``` ws = Workspace() ws.update_anomaly(UpdateAnomalyParams( id=123, status="dismissed", anomaly_class="Event" )) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def update_anomaly( self, params: UpdateAnomalyParams, ) -> dict[str, Any]: """Update the status of a single anomaly. Args: params: Update parameters with id, status, and anomaly_class. Returns: Raw API response as dict. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Anomaly not found or invalid parameters (400). Example: ```python ws = Workspace() ws.update_anomaly(UpdateAnomalyParams( id=123, status="dismissed", anomaly_class="Event" )) ``` """ client = self._require_api_client() return client.update_anomaly(params.model_dump(by_alias=True)) ```` ### bulk_update_anomalies ``` bulk_update_anomalies(params: BulkUpdateAnomalyParams) -> dict[str, Any] ``` Bulk update anomaly statuses. | PARAMETER | DESCRIPTION | | --------- | -------------------------------------------------------------------------------------- | | `params` | Bulk update with anomalies list and target status. **TYPE:** `BulkUpdateAnomalyParams` | | RETURNS | DESCRIPTION | | ---------------- | ------------------------- | | `dict[str, Any]` | Raw API response as dict. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Invalid parameters (400). | Example ``` ws = Workspace() ws.bulk_update_anomalies(BulkUpdateAnomalyParams( anomalies=[BulkAnomalyEntry(id=1, anomaly_class="Event")], status="dismissed", )) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def bulk_update_anomalies( self, params: BulkUpdateAnomalyParams, ) -> dict[str, Any]: """Bulk update anomaly statuses. Args: params: Bulk update with anomalies list and target status. Returns: Raw API response as dict. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Invalid parameters (400). Example: ```python ws = Workspace() ws.bulk_update_anomalies(BulkUpdateAnomalyParams( anomalies=[BulkAnomalyEntry(id=1, anomaly_class="Event")], status="dismissed", )) ``` """ client = self._require_api_client() return client.bulk_update_anomalies(params.model_dump(by_alias=True)) ```` ### list_deletion_requests ``` list_deletion_requests() -> list[EventDeletionRequest] ``` List all event deletion requests. | RETURNS | DESCRIPTION | | ---------------------------- | ------------------------------------- | | `list[EventDeletionRequest]` | List of EventDeletionRequest objects. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | Example ``` ws = Workspace() for r in ws.list_deletion_requests(): print(f"{r.event_name}: {r.status}") ``` Source code in `src/mixpanel_headless/workspace.py` ```` def list_deletion_requests(self) -> list[EventDeletionRequest]: """List all event deletion requests. Returns: List of ``EventDeletionRequest`` objects. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). Example: ```python ws = Workspace() for r in ws.list_deletion_requests(): print(f"{r.event_name}: {r.status}") ``` """ client = self._require_api_client() raw_list = client.list_deletion_requests() return [EventDeletionRequest.model_validate(r) for r in raw_list] ```` ### create_deletion_request ``` create_deletion_request( params: CreateDeletionRequestParams, ) -> list[EventDeletionRequest] ``` Create a new event deletion request. | PARAMETER | DESCRIPTION | | --------- | ---------------------------------------------------------------------------------------------------------------------- | | `params` | Deletion parameters with event_name, from_date, to_date, and optional filters. **TYPE:** `CreateDeletionRequestParams` | | RETURNS | DESCRIPTION | | ---------------------------- | --------------------------------------- | | `list[EventDeletionRequest]` | Updated full list of deletion requests. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Validation error (400). | Example ``` ws = Workspace() requests = ws.create_deletion_request( CreateDeletionRequestParams( event_name="Test", from_date="2026-01-01", to_date="2026-01-31", ) ) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def create_deletion_request( self, params: CreateDeletionRequestParams, ) -> list[EventDeletionRequest]: """Create a new event deletion request. Args: params: Deletion parameters with event_name, from_date, to_date, and optional filters. Returns: Updated full list of deletion requests. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Validation error (400). Example: ```python ws = Workspace() requests = ws.create_deletion_request( CreateDeletionRequestParams( event_name="Test", from_date="2026-01-01", to_date="2026-01-31", ) ) ``` """ client = self._require_api_client() raw_list = client.create_deletion_request( params.model_dump(exclude_none=True, by_alias=True) ) return [EventDeletionRequest.model_validate(r) for r in raw_list] ```` ### cancel_deletion_request ``` cancel_deletion_request(request_id: int) -> list[EventDeletionRequest] ``` Cancel a pending deletion request. | PARAMETER | DESCRIPTION | | ------------ | ---------------------------------------------- | | `request_id` | Deletion request ID to cancel. **TYPE:** `int` | | RETURNS | DESCRIPTION | | ---------------------------- | --------------------------------------- | | `list[EventDeletionRequest]` | Updated full list of deletion requests. | | RAISES | DESCRIPTION | | --------------------- | ------------------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Request not found or not cancellable (400). | Example ``` ws = Workspace() requests = ws.cancel_deletion_request(42) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def cancel_deletion_request(self, request_id: int) -> list[EventDeletionRequest]: """Cancel a pending deletion request. Args: request_id: Deletion request ID to cancel. Returns: Updated full list of deletion requests. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Request not found or not cancellable (400). Example: ```python ws = Workspace() requests = ws.cancel_deletion_request(42) ``` """ client = self._require_api_client() raw_list = client.cancel_deletion_request(request_id) return [EventDeletionRequest.model_validate(r) for r in raw_list] ```` ### preview_deletion_filters ``` preview_deletion_filters( params: PreviewDeletionFiltersParams, ) -> list[dict[str, Any]] ``` Preview what events a deletion filter would match. This is a read-only operation that does not modify any data. | PARAMETER | DESCRIPTION | | --------- | -------------------------------------------------------------------------------------------------------------- | | `params` | Preview parameters with event_name, date range, and optional filters. **TYPE:** `PreviewDeletionFiltersParams` | | RETURNS | DESCRIPTION | | ---------------------- | ------------------------------------ | | `list[dict[str, Any]]` | List of expanded/normalized filters. | | RAISES | DESCRIPTION | | --------------------- | --------------------------------- | | `ConfigError` | If credentials are not available. | | `AuthenticationError` | Invalid credentials (401). | | `QueryError` | Invalid filter parameters (400). | Example ``` ws = Workspace() preview = ws.preview_deletion_filters( PreviewDeletionFiltersParams( event_name="Test", from_date="2026-01-01", to_date="2026-01-31", ) ) ``` Source code in `src/mixpanel_headless/workspace.py` ```` def preview_deletion_filters( self, params: PreviewDeletionFiltersParams, ) -> list[dict[str, Any]]: """Preview what events a deletion filter would match. This is a read-only operation that does not modify any data. Args: params: Preview parameters with event_name, date range, and optional filters. Returns: List of expanded/normalized filters. Raises: ConfigError: If credentials are not available. AuthenticationError: Invalid credentials (401). QueryError: Invalid filter parameters (400). Example: ```python ws = Workspace() preview = ws.preview_deletion_filters( PreviewDeletionFiltersParams( event_name="Test", from_date="2026-01-01", to_date="2026-01-31", ) ) ``` """ client = self._require_api_client() return client.preview_deletion_filters( params.model_dump(exclude_none=True, by_alias=True) ) ```` Copy markdown # Auth The auth surface in `mixpanel_headless` is organized around three independent axes β€” Account, Project, Workspace β€” with three first-class account types, a single resolver, fluent in-session switching via `Workspace.use()`, and a Cowork bridge for remote authentication. Explore on DeepWiki πŸ€– **[Configuration Reference β†’](https://deepwiki.com/mixpanel/mixpanel-headless/7.3-configuration-reference)** Ask questions about account types, session axes, OAuth, the Cowork bridge, or in-session switching. ## Overview ``` import mixpanel_headless as mp # Construct a Workspace from active config ws = mp.Workspace() # Override per Workspace (env > param > target > bridge > [active] > default_project) ws = mp.Workspace(account="team", project="3713224") ws = mp.Workspace(target="ecom") ws = mp.Workspace(session=mp.Session(account=..., project=..., workspace=...)) # In-session switching β€” fluent, O(1), no re-auth on project swap ws.use(project="3018488").events() ws.use(account="personal").events() # rebuilds auth header; preserves underlying HTTP client ws.use(target="ecom").events() # applies all three axes atomically ws.use(workspace=3448414).events() # Functional namespaces (also re-exported as mp.accounts / mp.session / mp.targets) summaries = mp.accounts.list() mp.accounts.use("team") active = mp.session.show() # ActiveSession mp.targets.add("ecom", account="team", project="3018488", workspace=3448414) ``` See [Configuration](https://mixpanel.github.io/mixpanel-headless/getting-started/configuration/index.md) for the full setup walkthrough. ## Account Types `Account` is a Pydantic discriminated union over three first-class variants. The `type` field selects the variant; each variant carries the credentials it needs. ``` from mixpanel_headless import ( Account, # discriminated union type ServiceAccount, # type == "service_account" OAuthBrowserAccount, # type == "oauth_browser" OAuthTokenAccount, # type == "oauth_token" AccountType, # Literal["service_account" | "oauth_browser" | "oauth_token"] Region, # Literal["us" | "eu" | "in"] ) account: Account = ServiceAccount( name="team", region="us", default_project="3018488", username="team-mp...", secret="...", ) if isinstance(account, ServiceAccount): print(f"SA {account.name} β†’ project {account.default_project}") ``` ### ServiceAccount Long-lived HTTP Basic Auth credentials. Best for CI / scripts / unattended automation. ## mixpanel_headless.ServiceAccount Bases: `_AccountBase` Basic-auth service account credentials. Long-lived credentials provisioned via the Mixpanel UI ("Service Accounts" section). Encodes `username:secret` as base64 for the `Authorization` header per the Mixpanel REST API spec. Example ``` sa = ServiceAccount( name="team", region="us", username="sa.user", secret=SecretStr("hunter2"), ) header = sa.auth_header(token_resolver=None) # "Basic c2EudXNlcjpodW50ZXIy" ``` ### type ``` type: Literal['service_account'] = 'service_account' ``` Discriminator value for this variant. ### username ``` username: Annotated[str, Field(min_length=1)] ``` Service account username (e.g. `sa.demo`). ### secret ``` secret: SecretStr ``` Service account secret. Redacted in repr/str via Pydantic. ### auth_header ``` auth_header(*, token_resolver: TokenResolver | None = None) -> str ``` Return the `Authorization` header value for HTTP requests. | PARAMETER | DESCRIPTION | | ---------------- | ----------------------------------------------------------------------------------------------------------- | | `token_resolver` | Ignored for service accounts (kept for signature parity with the other variants). **TYPE:** \`TokenResolver | | RETURNS | DESCRIPTION | | ------- | -------------------------------- | | `str` | The Basic header value. | Source code in `src/mixpanel_headless/_internal/auth/account.py` ``` def auth_header( self, *, token_resolver: TokenResolver | None = None, # noqa: ARG002 β€” signature parity with OAuth variants ) -> str: """Return the ``Authorization`` header value for HTTP requests. Args: token_resolver: Ignored for service accounts (kept for signature parity with the other variants). Returns: The ``Basic `` header value. """ raw = f"{self.username}:{self.secret.get_secret_value()}" encoded = base64.b64encode(raw.encode()).decode("ascii") return f"Basic {encoded}" ``` ### is_long_lived ``` is_long_lived() -> bool ``` Return whether this account survives across restarts without refresh. | RETURNS | DESCRIPTION | | ------- | ------------------------------------------------ | | `bool` | True β€” service account credentials never expire. | Source code in `src/mixpanel_headless/_internal/auth/account.py` ``` def is_long_lived(self) -> bool: """Return whether this account survives across restarts without refresh. Returns: ``True`` β€” service account credentials never expire. """ return True ``` ### OAuthBrowserAccount PKCE browser flow; access/refresh tokens persisted at `~/.mp/accounts/{name}/tokens.json` and auto-refreshed on expiry. ## mixpanel_headless.OAuthBrowserAccount Bases: `_AccountBase` OAuth account authenticated via PKCE browser flow. The `Account` itself carries no secret β€” tokens are persisted at `~/.mp/accounts/{name}/tokens.json` and produced on demand by a :class:`TokenResolver`. Example ``` a = OAuthBrowserAccount(name="me", region="us") header = a.auth_header(token_resolver=resolver) # "Bearer " ``` ### type ``` type: Literal['oauth_browser'] = 'oauth_browser' ``` Discriminator value for this variant. ### auth_header ``` auth_header(*, token_resolver: TokenResolver) -> str ``` Return the `Authorization` header value for HTTP requests. | PARAMETER | DESCRIPTION | | ---------------- | ---------------------------------------------------------------------------------------------------- | | `token_resolver` | Resolver responsible for loading + refreshing the on-disk token. Required. **TYPE:** `TokenResolver` | | RETURNS | DESCRIPTION | | ------- | -------------------------------- | | `str` | The Bearer header value. | Source code in `src/mixpanel_headless/_internal/auth/account.py` ``` def auth_header(self, *, token_resolver: TokenResolver) -> str: """Return the ``Authorization`` header value for HTTP requests. Args: token_resolver: Resolver responsible for loading + refreshing the on-disk token. Required. Returns: The ``Bearer `` header value. """ token = token_resolver.get_browser_token(self.name, self.region) return f"Bearer {token}" ``` ### is_long_lived ``` is_long_lived() -> bool ``` Return whether this account survives across restarts without refresh. | RETURNS | DESCRIPTION | | ------- | --------------------------------------------------------------- | | `bool` | True β€” refresh-token-driven re-issuance keeps the bearer valid. | Source code in `src/mixpanel_headless/_internal/auth/account.py` ``` def is_long_lived(self) -> bool: """Return whether this account survives across restarts without refresh. Returns: ``True`` β€” refresh-token-driven re-issuance keeps the bearer valid. """ return True ``` ### OAuthTokenAccount Static bearer token (CI / agents) β€” supplied inline or via an env var (`token_env`). ## mixpanel_headless.OAuthTokenAccount Bases: `_AccountBase` OAuth account using a static bearer token (CI, agents, ephemeral runs). Exactly one of `token` (inline `SecretStr`) or `token_env` (env-var name) must be provided β€” never both, never neither. This is enforced at construction time by :meth:`_validate_exactly_one_token_source`. Example ``` OAuthTokenAccount(name="ci", region="us", token=SecretStr("xyz")) OAuthTokenAccount(name="agent", region="eu", token_env="MP_OAUTH_TOKEN") ``` ### type ``` type: Literal['oauth_token'] = 'oauth_token' ``` Discriminator value for this variant. ### token ``` token: SecretStr | None = None ``` Inline static bearer token (mutually exclusive with `token_env`). ### token_env ``` token_env: str | None = None ``` Env-var name to read the bearer from at resolution time. ### auth_header ``` auth_header(*, token_resolver: TokenResolver) -> str ``` Return the `Authorization` header value for HTTP requests. | PARAMETER | DESCRIPTION | | ---------------- | ------------------------------------------------------------------------------------------------------------------------ | | `token_resolver` | Resolver responsible for materializing the token (from inline SecretStr or env var). Required. **TYPE:** `TokenResolver` | | RETURNS | DESCRIPTION | | ------- | -------------------------------- | | `str` | The Bearer header value. | Source code in `src/mixpanel_headless/_internal/auth/account.py` ``` def auth_header(self, *, token_resolver: TokenResolver) -> str: """Return the ``Authorization`` header value for HTTP requests. Args: token_resolver: Resolver responsible for materializing the token (from inline ``SecretStr`` or env var). Required. Returns: The ``Bearer `` header value. """ token = token_resolver.get_static_token(self) return f"Bearer {token}" ``` ### is_long_lived ``` is_long_lived() -> bool ``` Return whether this account survives across restarts without refresh. | RETURNS | DESCRIPTION | | ------- | ------------------------------------------------------------ | | `bool` | False β€” the caller controls token rotation; no refresh path. | Source code in `src/mixpanel_headless/_internal/auth/account.py` ``` def is_long_lived(self) -> bool: """Return whether this account survives across restarts without refresh. Returns: ``False`` β€” the caller controls token rotation; no refresh path. """ return False ``` ## Session Axes A `Session` is the immutable resolved state for a single Workspace at construction time β€” account, project, optional workspace, and the auth headers they generate. ## mixpanel_headless.Session Bases: `BaseModel` Immutable in-memory tuple of (Account, Project, optional WorkspaceRef). Holds the resolved auth/scope state for a single chain of API calls. Switching to a different account, project, or workspace produces a new Session via :meth:`replace`; the original is never mutated. Workspace is optional: a session with `workspace=None` lazy-resolves on the first workspace-scoped API call (per FR-025). ### account ``` account: Account ``` Resolved account (one of the three discriminated variants). ### project ``` project: Project ``` Resolved Mixpanel project. ### workspace ``` workspace: WorkspaceRef | None = None ``` Resolved workspace; `None` triggers lazy resolution on first use. ### headers ``` headers: Mapping[str, str] = Field(default_factory=dict) ``` Custom HTTP headers attached at resolution time. Populated from `[settings].custom_header` and/or `bridge.headers`. Never read from `os.environ` after Session construction (per FR-014). Wrapped in :class:`types.MappingProxyType` after validation, so any in-place mutation (`session.headers["X"] = "Y"`) raises :class:`TypeError` instead of silently sharing state across sessions. Consumers that need a mutable copy should use `dict(session.headers)`. ### project_id ``` project_id: str ``` Return the project's numeric string ID. | RETURNS | DESCRIPTION | | ------- | ---------------- | | `str` | self.project.id. | ### workspace_id ``` workspace_id: int | None ``` Return the workspace ID if set, else `None`. | RETURNS | DESCRIPTION | | ------- | ----------- | | \`int | None\` | ### region ``` region: Region ``` Return the account's region. | RETURNS | DESCRIPTION | | -------- | -------------------- | | `Region` | self.account.region. | ### auth_header ``` auth_header(*, token_resolver: TokenResolver | None) -> str ``` Return the `Authorization` header for HTTP requests. | PARAMETER | DESCRIPTION | | ---------------- | ---------------------------------------------------------------------------------- | | `token_resolver` | Required for OAuth accounts; ignored for ServiceAccount. **TYPE:** \`TokenResolver | | RETURNS | DESCRIPTION | | ------- | ------------------------------------------- | | `str` | The header value (Basic ... or Bearer ...). | Source code in `src/mixpanel_headless/_internal/auth/session.py` ``` def auth_header(self, *, token_resolver: TokenResolver | None) -> str: """Return the ``Authorization`` header for HTTP requests. Args: token_resolver: Required for OAuth accounts; ignored for ``ServiceAccount``. Returns: The header value (``Basic ...`` or ``Bearer ...``). """ # Only OAuth variants need a resolver; type-narrowed by the discriminator. if self.account.type == "service_account": return self.account.auth_header(token_resolver=token_resolver) if token_resolver is None: raise TypeError( "TokenResolver is required to compute auth_header for OAuth accounts" ) return self.account.auth_header(token_resolver=token_resolver) ``` ### replace ``` replace( *, account: Account | None = None, project: Project | None = None, workspace: WorkspaceRef | None | _SentinelType = _SENTINEL, headers: Mapping[str, str] | _SentinelType = _SENTINEL, ) -> Session ``` Return a new Session with the supplied axes swapped in. Workspace and headers use a sentinel because `None` (resp. `{}`) is a valid replacement value, semantically distinct from "do not touch this axis". | PARAMETER | DESCRIPTION | | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | | `account` | Replacement account; omitted preserves the current value. **TYPE:** \`Account | | `project` | Replacement project; omitted preserves the current value. **TYPE:** \`Project | | `workspace` | Replacement workspace; None clears the workspace (re-triggering lazy resolution); omitting the kwarg preserves the current value. **TYPE:** \`WorkspaceRef | | `headers` | Replacement headers map; {} clears all custom headers; omitting the kwarg preserves the current value. **TYPE:** \`Mapping[str, str] | | RETURNS | DESCRIPTION | | --------- | --------------------------------------------------------- | | `Session` | A new :class:Session instance; the original is unchanged. | Source code in `src/mixpanel_headless/_internal/auth/session.py` ``` def replace( self, *, account: Account | None = None, project: Project | None = None, workspace: WorkspaceRef | None | _SentinelType = _SENTINEL, headers: Mapping[str, str] | _SentinelType = _SENTINEL, ) -> Session: """Return a new Session with the supplied axes swapped in. Workspace and headers use a sentinel because ``None`` (resp. ``{}``) is a valid replacement value, semantically distinct from "do not touch this axis". Args: account: Replacement account; omitted preserves the current value. project: Replacement project; omitted preserves the current value. workspace: Replacement workspace; ``None`` clears the workspace (re-triggering lazy resolution); omitting the kwarg preserves the current value. headers: Replacement headers map; ``{}`` clears all custom headers; omitting the kwarg preserves the current value. Returns: A new :class:`Session` instance; the original is unchanged. """ update: dict[str, Any] = {} if account is not None: update["account"] = account if project is not None: update["project"] = project if workspace is not _SENTINEL: update["workspace"] = workspace if headers is not _SENTINEL: update["headers"] = headers return self.model_copy(update=update) ``` ## mixpanel_headless.Project Bases: `BaseModel` Mixpanel project reference. Project IDs come from the Mixpanel API as numeric strings. `timezone` and `organization_id` are populated when the resolver has access to a `/me` response; both are optional. ### id ``` id: Annotated[ProjectId, Field(min_length=1, pattern='^\\d+$')] ``` Numeric project ID (Mixpanel's wire format is a digit string). ### name ``` name: str | None = None ``` Display name from `/me`, when known. ### organization_id ``` organization_id: int | None = None ``` Owning organization ID from `/me`, when known. ### timezone ``` timezone: str | None = None ``` Project timezone (e.g. `"US/Pacific"`) from `/me`, when known. ## mixpanel_headless.WorkspaceRef Bases: `BaseModel` Mixpanel workspace reference (cohort/dashboard scoping unit). The data model is named `WorkspaceRef` to avoid colliding with the public `Workspace` facade class. Public re-export keeps the `WorkspaceRef` name. The optional `project_id` lets a :class:`Session` cross-check that the workspace actually belongs to the bound project β€” every workspace lives inside exactly one project, and routing a workspace ID to the wrong project returns 400/404 deep inside the API call rather than at session construction. When populated (typically from a `/me` enumeration), the session-level model_validator raises :class:`ValueError` on mismatch instead of letting the bug surface as an HTTP error mid-query. ### id ``` id: Annotated[WorkspaceId, Field(gt=0)] ``` Positive integer workspace ID assigned by Mixpanel. ### name ``` name: str | None = None ``` Display name from `/me` or `/projects/{pid}/workspaces/public`. ### is_default ``` is_default: bool | None = None ``` Whether this is the project's default workspace, when known. ### project_id ``` project_id: ProjectId | None = None ``` Owning project ID, when known. Populated by `/me` enumeration paths so :class:`Session` can verify project ↔ workspace coupling. Left `None` when the workspace was constructed from a bare ID (e.g. `MP_WORKSPACE_ID=N`) β€” in that case the cross-check degrades to "trust the caller" rather than raising a spurious mismatch error. ## mixpanel_headless.auth_types.ActiveSession Bases: `BaseModel` Persisted shape of the `[active]` block in `~/.mp/config.toml`. Only `account` and `workspace` live in `[active]`. Project lives on the account itself as `Account.default_project` β€” switching accounts implicitly switches projects. Unknown keys (including `project`) are rejected by `extra="forbid"`. Both fields are optional β€” environment variables or per-command flags can supply each one independently. ### account ``` account: AccountName | None = None ``` Local config name of the active account (must reference `[accounts.NAME]`). ### workspace ``` workspace: WorkspaceId | None = None ``` Active workspace ID (positive int) or `None` for lazy resolution. ## Workspace.use() β€” In-Session Switching `Workspace.use()` is the only in-session switching method. It returns `self` for fluent chaining and preserves the underlying `httpx.Client` and per-account `/me` cache across switches, so cross-project / cross-account iteration is O(1) per turn. ``` import mixpanel_headless as mp ws = mp.Workspace() # active session # In-session switching (returns self for chaining) ws.use(account="team") # implicitly clears workspace ws.use(project="3018488") ws.use(workspace=3448414) ws.use(target="ecom") # apply all three at once # Persist the new state ws.use(project="3018488", persist=True) # writes account.default_project; [active] only stores account + workspace # Fluent chain result = ws.use(project="3018488").segmentation( "Login", from_date="2026-04-01", to_date="2026-04-21" ) ``` Switching the active account clears the workspace (workspaces are project-scoped). The project re-resolves on account swap via `env > explicit > new account's default_project`. There is **no silent cross-axis fallback**: if an axis can't be resolved on the new account, `use()` raises `ConfigError`. Swap one or more session axes in place; return `self` for chaining. `target=` is mutually exclusive with `account=`/`project=`/ `workspace=`. The HTTP transport is preserved across all switches (per Research R5). When `account=` is supplied, the project axis re-resolves through the FR-017 chain ending at the new account's `default_project` (env `MP_PROJECT_ID` > explicit `project=` > new account's `default_project`). If no source provides a project, the call raises :class:`ConfigError` per FR-033 β€” the prior session's project is NEVER carried forward across an account swap because cross-account project access is not guaranteed. The workspace axis is cleared on account swap (workspaces are project-scoped; the prior workspace doesn't apply to the new project) β€” explicit `workspace=` or `MP_WORKSPACE_ID` env override is honored. | PARAMETER | DESCRIPTION | | ----------- | -------------------------------------------------------------------------------------- | | `account` | Replacement account name. **TYPE:** \`str | | `project` | Replacement project ID. **TYPE:** \`str | | `workspace` | Replacement workspace ID. **TYPE:** \`int | | `target` | Apply this target's three axes atomically. **TYPE:** \`str | | `persist` | When True, also write the new state to [active]. **TYPE:** `bool` **DEFAULT:** `False` | | RETURNS | DESCRIPTION | | ----------- | ------------------------- | | `Workspace` | self for fluent chaining. | | RAISES | DESCRIPTION | | ------------- | ------------------------------------------------------- | | `ValueError` | Mutually exclusive args, or referenced name missing. | | `OAuthError` | New auth header construction fails (atomic on success). | | `ConfigError` | account= swap cannot resolve a project axis. | Source code in `src/mixpanel_headless/workspace.py` ``` def use( self, *, account: str | None = None, project: str | None = None, workspace: int | None = None, target: str | None = None, persist: bool = False, ) -> Workspace: """Swap one or more session axes in place; return ``self`` for chaining. ``target=`` is mutually exclusive with ``account=``/``project=``/ ``workspace=``. The HTTP transport is preserved across all switches (per Research R5). When ``account=`` is supplied, the project axis re-resolves through the FR-017 chain ending at the new account's ``default_project`` (env ``MP_PROJECT_ID`` > explicit ``project=`` > new account's ``default_project``). If no source provides a project, the call raises :class:`ConfigError` per FR-033 β€” the prior session's project is NEVER carried forward across an account swap because cross-account project access is not guaranteed. The workspace axis is cleared on account swap (workspaces are project-scoped; the prior workspace doesn't apply to the new project) β€” explicit ``workspace=`` or ``MP_WORKSPACE_ID`` env override is honored. Args: account: Replacement account name. project: Replacement project ID. workspace: Replacement workspace ID. target: Apply this target's three axes atomically. persist: When ``True``, also write the new state to ``[active]``. Returns: ``self`` for fluent chaining. Raises: ValueError: Mutually exclusive args, or referenced name missing. OAuthError: New auth header construction fails (atomic on success). ConfigError: ``account=`` swap cannot resolve a project axis. """ if target is not None and ( account is not None or project is not None or workspace is not None ): raise ValueError( "`target=` is mutually exclusive with `account=`/`project=`/`workspace=`." ) cm = ConfigManager() client = self._require_api_client() new_account_obj: _AccountUnion | None = None new_project_obj: _Project | None = None new_workspace_obj: _WorkspaceRef | None = None if target is not None: # Route through the same resolver as Workspace() construction so # env > param > target > bridge > config ordering applies (FR-017). # Without this, mid-process env-var overrides would be honored at # construction but silently ignored on `ws.use(target=...)`. sess = _resolve_session( target=target, config=cm, bridge=_load_bridge(), ) new_account_obj = sess.account new_project_obj = sess.project new_workspace_obj = sess.workspace elif account is not None: # Explicit account swap: the user told us which account to use, # so the env-vars-override-param rule (FR-017) on the account # axis doesn't apply here β€” load the requested account directly. # Project re-resolves through the FR-017 chain ending at the # NEW account's default_project (env > explicit > new account's # default); raises ConfigError if nothing resolves (per FR-033, # cross-account project access is not guaranteed). # Workspace is cleared (workspaces are project-scoped; the # prior workspace is meaningless under the new account/project) # β€” explicit `workspace=` overrides the clear, and env override # via MP_WORKSPACE_ID still applies for parity with FR-017. new_account_obj = cm.get_account(account) br = _load_bridge() project_id = _resolve_project_axis( explicit=project, target_project=None, bridge=br, account=new_account_obj, ) if project_id is None: raise ConfigError(_format_no_project_error(new_account_obj)) new_project_obj = _Project(id=project_id) # Account-swap intentionally clears workspace per FR-033 (workspaces # are project-scoped; the prior workspace doesn't apply to the new # project). Only an explicit ``workspace=`` kwarg or a validated # ``MP_WORKSPACE_ID`` env var can populate it. We bypass # ``resolve_workspace_axis`` because that consults ``[active].workspace`` # β€” which is exactly the fallback we need to skip here. if workspace is not None: new_workspace_obj = _WorkspaceRef(id=workspace) else: env_ws = _env_workspace_id() new_workspace_obj = ( _WorkspaceRef(id=env_ws) if env_ws is not None else None ) else: new_project_obj = _Project(id=project) if project is not None else None new_workspace_obj = ( _WorkspaceRef(id=workspace) if workspace is not None else None ) client.use( account=new_account_obj, project=new_project_obj, workspace=new_workspace_obj, ) self._session = client.session # Clear lazy services so subsequent reads of `project` / `account` / # `workspaces()` / `_me_svc` observe the new session rather than the # prior one. self._account_name = self._session.account.name self._initial_workspace_id = ( self._session.workspace.id if self._session.workspace else None ) self._discovery = None self._live_query = None self._me_service = None if persist: self._persist_active() return self ``` ### Snapshot mode (parallel iteration) For parallel cross-project iteration, snapshot the resolved `Session` and construct a fresh `Workspace` per task: ``` from concurrent.futures import ThreadPoolExecutor import mixpanel_headless as mp ws = mp.Workspace() sessions = [ ws.session.replace(project=mp.Project(id=p.id)) for p in ws.projects() ] def event_count(s: mp.Session) -> int: return len(mp.Workspace(session=s).events()) with ThreadPoolExecutor(max_workers=4) as pool: counts = list(pool.map(event_count, sessions)) ``` ## Functional Namespaces The auth surface exposes three module-level namespaces re-exported from `mixpanel_headless`. These are the canonical Python API for managing accounts, the active session, and saved targets. ### `mp.accounts` Account lifecycle: register, switch, probe, OAuth flows, bridge export. The `login_unified` orchestrator below collapses the explicit `add` + `login` pair into a single conversational call (the Python entry point behind `mp login`). ## mixpanel_headless.accounts Public `mp.accounts` namespace. Thin wrapper around :class:`~mixpanel_headless._internal.config.ConfigManager` exposing account CRUD, switching, and probing operations as the canonical Python API for `mp account ...` CLI commands and the plugin's `auth_manager.py`. Reference: specs/042-auth-architecture-redesign/contracts/python-api.md Β§5. ### list ``` list() -> builtins.list[AccountSummary] ``` Return all configured accounts as `AccountSummary` records. | RETURNS | DESCRIPTION | | ---------------------- | --------------------------------- | | `list[AccountSummary]` | Sorted-by-name list of summaries. | Source code in `src/mixpanel_headless/accounts.py` ``` def list() -> builtins.list[AccountSummary]: # noqa: A001 β€” public namespace shadow """Return all configured accounts as ``AccountSummary`` records. Returns: Sorted-by-name list of summaries. """ return _config().list_accounts() ``` ### add ``` add( name: str | None = None, *, type: AccountType, region: Region | None = None, default_project: str | None = None, username: str | None = None, secret: SecretStr | str | None = None, token: SecretStr | str | None = None, token_env: str | None = None, derive_name: bool = False, ) -> AccountSummary ``` Add a new account. Per 043 FR-001, `default_project` is OPTIONAL for every account type at add-time. Service-account and oauth_token callers can backfill it later via `mp project use ID` (or the `mp login` orchestrator's project picker). For `oauth_browser` the value is additionally backfilled by `login(name)` from the `/me` lookup when no explicit project was set at add-time. Per FR-045, the first account added auto-promotes to `[active].account`. Subsequent accounts do not. ##### Derived naming (specs/043-frictionless-auth) Pass `derive_name=True` (and leave `name=None`) to have the function fetch `/me` against the supplied credentials and pick a name from the first organization via :func:`naming.default_account_name`. `derive_name=True` together with an explicit `name=` is a logic error and raises `TypeError` to surface the conflict at the caller. Derivation is only supported for `service_account` and `oauth_token` β€” the `oauth_browser` path needs the PKCE flow to obtain credentials, which lives in `mp login` / `login_unified` (not here). | PARAMETER | DESCRIPTION | | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `name` | Account name (must match ^[a-zA-Z0-9\_-]{1,64}$). Required unless derive_name=True. **TYPE:** \`str | | `type` | One of service_account / oauth_browser / oauth_token. **TYPE:** `AccountType` | | `region` | One of us / eu / in. May be omitted only for oauth_browser (the PKCE flow commits to the account's stored region at login time). For service_account and oauth_token, region=None raises ConfigError β€” the Python API does not probe; pass --region to the CLI or use mp login for the guided probing flow. **TYPE:** \`Region | | `default_project` | Numeric project ID. Optional for every type; populated later via mp project use or mp login. **TYPE:** \`str | | `username` | Required for service_account. **TYPE:** \`str | | `secret` | Required for service_account. **TYPE:** \`SecretStr | | `token` | For oauth_token (mutually exclusive with token_env). **TYPE:** \`SecretStr | | `token_env` | For oauth_token (mutually exclusive with token). **TYPE:** \`str | | `derive_name` | When True, fetch /me and pick a name via :func:naming.default_account_name. Mutually exclusive with name= (passing both raises TypeError). Not supported for oauth_browser. **TYPE:** `bool` **DEFAULT:** `False` | | RETURNS | DESCRIPTION | | ---------------- | ----------------------------------------- | | `AccountSummary` | class:AccountSummary for the new account. | | RAISES | DESCRIPTION | | ------------- | -------------------------------------------------------------------------------------------------------------- | | `TypeError` | derive_name=True with explicit name=..., or derive_name=False with name=None. | | `ConfigError` | Validation failure, duplicate name, region=None for a non-browser type, or derive_name=True for oauth_browser. | Source code in `src/mixpanel_headless/accounts.py` ``` def add( name: str | None = None, *, type: AccountType, # noqa: A002 β€” matches contracts/python-api.md region: Region | None = None, default_project: str | None = None, username: str | None = None, secret: SecretStr | str | None = None, token: SecretStr | str | None = None, token_env: str | None = None, derive_name: bool = False, ) -> AccountSummary: """Add a new account. Per 043 FR-001, ``default_project`` is OPTIONAL for every account type at add-time. Service-account and oauth_token callers can backfill it later via ``mp project use ID`` (or the ``mp login`` orchestrator's project picker). For ``oauth_browser`` the value is additionally backfilled by ``login(name)`` from the ``/me`` lookup when no explicit project was set at add-time. Per FR-045, the first account added auto-promotes to ``[active].account``. Subsequent accounts do not. ## Derived naming (specs/043-frictionless-auth) Pass ``derive_name=True`` (and leave ``name=None``) to have the function fetch ``/me`` against the supplied credentials and pick a name from the first organization via :func:`naming.default_account_name`. ``derive_name=True`` together with an explicit ``name=`` is a logic error and raises ``TypeError`` to surface the conflict at the caller. Derivation is only supported for ``service_account`` and ``oauth_token`` β€” the ``oauth_browser`` path needs the PKCE flow to obtain credentials, which lives in ``mp login`` / ``login_unified`` (not here). Args: name: Account name (must match ``^[a-zA-Z0-9_-]{1,64}$``). Required unless ``derive_name=True``. type: One of ``service_account`` / ``oauth_browser`` / ``oauth_token``. region: One of ``us`` / ``eu`` / ``in``. May be omitted only for ``oauth_browser`` (the PKCE flow commits to the account's stored region at login time). For ``service_account`` and ``oauth_token``, ``region=None`` raises ``ConfigError`` β€” the Python API does not probe; pass ``--region`` to the CLI or use ``mp login`` for the guided probing flow. default_project: Numeric project ID. Optional for every type; populated later via ``mp project use`` or ``mp login``. username: Required for ``service_account``. secret: Required for ``service_account``. token: For ``oauth_token`` (mutually exclusive with ``token_env``). token_env: For ``oauth_token`` (mutually exclusive with ``token``). derive_name: When ``True``, fetch ``/me`` and pick a name via :func:`naming.default_account_name`. Mutually exclusive with ``name=`` (passing both raises ``TypeError``). Not supported for ``oauth_browser``. Returns: :class:`AccountSummary` for the new account. Raises: TypeError: ``derive_name=True`` with explicit ``name=...``, or ``derive_name=False`` with ``name=None``. ConfigError: Validation failure, duplicate name, ``region=None`` for a non-browser type, or ``derive_name=True`` for ``oauth_browser``. """ if derive_name and name is not None: raise TypeError( "`derive_name=True` and explicit `name=` are mutually exclusive." ) if not derive_name and name is None: raise TypeError("`name` is required unless `derive_name=True`.") cm = _config() # Per 043 plan Β§"Library-First": region probing lives in the CLI # layer (where the per-attempt stderr narration is appropriate). # The Python API stays pure β€” it refuses to invent a region. if region is None and type != "oauth_browser": raise ConfigError( f"Account type {type!r} requires `region`. Pass region= " "explicitly, or use `mp login` for the guided probing flow." ) # ``oauth_browser`` may default to ``us`` when no explicit region is # supplied β€” the PKCE flow commits to the account's stored region at # login time, and the post-callback ``/me`` cross-check will # surface a mismatch with an actionable error if the user picks a # project from a different cluster. resolved_region: Region = region if region is not None else "us" if derive_name: if type == "oauth_browser": raise ConfigError( "`derive_name=True` is not supported for oauth_browser. " "Use `mp login` (or `accounts.login_unified`) β€” the " "browser flow needs PKCE before /me can be reached." ) name = _derive_account_name_for_credential( cm, account_type=type, region=resolved_region, username=username, secret=secret, token=token, token_env=token_env, ) # ``derive_name`` and the ``not derive_name`` branch both leave # ``name`` populated by this point; the assert is a guard for the # static checker so the downstream call sites typecheck against # ``name: str`` rather than ``str | None``. assert name is not None # Compose the add-and-promote-as-active sequence in a single _mutate() # transaction so a fresh process never sees the new account without its # promoted [active].account when it was the first account added. with cm._mutate() as raw: is_first = not (raw.get("accounts") or {}) cm._apply_add_account( raw, name, type=type, region=resolved_region, default_project=default_project, username=username, secret=secret, token=token, token_env=token_env, ) if is_first: cm._apply_set_active(raw, account=name) return show(name) ``` ### update ``` update( name: str, *, region: Region | None = None, default_project: str | None = None, username: str | None = None, secret: SecretStr | str | None = None, token: SecretStr | str | None = None, token_env: str | None = None, ) -> AccountSummary ``` Update fields on an existing account in place. Type cannot be changed via this function (remove + re-add for that). Type-incompatible fields raise `ConfigError`. | PARAMETER | DESCRIPTION | | ----------------- | ---------------------------------------------------------- | | `name` | Account to update. **TYPE:** `str` | | `region` | New region. **TYPE:** \`Region | | `default_project` | New default_project (digit string). **TYPE:** \`str | | `username` | New username (service_account only). **TYPE:** \`str | | `secret` | New secret (service_account only). **TYPE:** \`SecretStr | | `token` | New inline token (oauth_token only). **TYPE:** \`SecretStr | | `token_env` | New env-var name (oauth_token only). **TYPE:** \`str | | RETURNS | DESCRIPTION | | --------- | ------------------------------------------------ | | `Updated` | class:AccountSummary. **TYPE:** `AccountSummary` | | RAISES | DESCRIPTION | | ------------- | ------------------------------------------------------------------ | | `ConfigError` | Account not found, type-incompatible field, or validation failure. | Source code in `src/mixpanel_headless/accounts.py` ``` def update( name: str, *, region: Region | None = None, default_project: str | None = None, username: str | None = None, secret: SecretStr | str | None = None, token: SecretStr | str | None = None, token_env: str | None = None, ) -> AccountSummary: """Update fields on an existing account in place. Type cannot be changed via this function (remove + re-add for that). Type-incompatible fields raise ``ConfigError``. Args: name: Account to update. region: New region. default_project: New ``default_project`` (digit string). username: New username (service_account only). secret: New secret (service_account only). token: New inline token (oauth_token only). token_env: New env-var name (oauth_token only). Returns: Updated :class:`AccountSummary`. Raises: ConfigError: Account not found, type-incompatible field, or validation failure. """ _config().update_account( name, region=region, default_project=default_project, username=username, secret=secret, token=token, token_env=token_env, ) return show(name) ``` ### remove ``` remove(name: str, *, force: bool = False) -> builtins.list[str] ``` Remove an account. | PARAMETER | DESCRIPTION | | --------- | -------------------------------------------------------------------------------------- | | `name` | Account name. **TYPE:** `str` | | `force` | When True, remove even if referenced by targets. **TYPE:** `bool` **DEFAULT:** `False` | | RETURNS | DESCRIPTION | | ----------- | ---------------------------------------------------------- | | `list[str]` | List of orphaned target names (empty unless force=True and | | `list[str]` | the account had references). | | RAISES | DESCRIPTION | | ------------------- | --------------------------- | | `ConfigError` | Missing account. | | `AccountInUseError` | Referenced and force=False. | Source code in `src/mixpanel_headless/accounts.py` ``` def remove(name: str, *, force: bool = False) -> builtins.list[str]: """Remove an account. Args: name: Account name. force: When ``True``, remove even if referenced by targets. Returns: List of orphaned target names (empty unless ``force=True`` and the account had references). Raises: ConfigError: Missing account. AccountInUseError: Referenced and ``force=False``. """ return _config().remove_account(name, force=force) ``` ### use ``` use(name: str) -> None ``` Switch the active account, clearing any prior workspace pin. The new account becomes `[active].account` and any prior `[active].workspace` is dropped β€” workspaces are project-scoped, so a leftover workspace ID from a different account would resolve to a foreign workspace (or a 404) on the next `Workspace()` construction. Project lives on the account itself as :attr:`Account.default_project`, so it travels with the new account automatically β€” no separate project axis to reset. Both writes happen in a single `_mutate()` transaction so the next process never sees a half-swapped state. | PARAMETER | DESCRIPTION | | --------- | --------------------------------------- | | `name` | Account to make active. **TYPE:** `str` | | RAISES | DESCRIPTION | | ------------- | ----------------------- | | `ConfigError` | Account does not exist. | Source code in `src/mixpanel_headless/accounts.py` ``` def use(name: str) -> None: """Switch the active account, clearing any prior workspace pin. The new account becomes ``[active].account`` and any prior ``[active].workspace`` is dropped β€” workspaces are project-scoped, so a leftover workspace ID from a different account would resolve to a foreign workspace (or a 404) on the next ``Workspace()`` construction. Project lives on the account itself as :attr:`Account.default_project`, so it travels with the new account automatically β€” no separate project axis to reset. Both writes happen in a single ``_mutate()`` transaction so the next process never sees a half-swapped state. Args: name: Account to make active. Raises: ConfigError: Account does not exist. """ cm = _config() with cm._mutate() as raw: cm._apply_set_active(raw, account=name) cm._apply_clear_active(raw, workspace=True) ``` ### show ``` show(name: str | None = None) -> AccountSummary ``` Return the named account summary, or the active one if no name given. | PARAMETER | DESCRIPTION | | --------- | ------------------------------------------------------------------- | | `name` | Account name; if None, the active account is shown. **TYPE:** \`str | | RETURNS | DESCRIPTION | | ---------------- | --------------------- | | `AccountSummary` | class:AccountSummary. | | RAISES | DESCRIPTION | | ------------- | -------------------------------------------------- | | `ConfigError` | Account not found OR no active account configured. | Source code in `src/mixpanel_headless/accounts.py` ``` def show(name: str | None = None) -> AccountSummary: """Return the named account summary, or the active one if no name given. Args: name: Account name; if ``None``, the active account is shown. Returns: :class:`AccountSummary`. Raises: ConfigError: Account not found OR no active account configured. """ cm = _config() if name is None: active = cm.get_active().account if not active: raise ConfigError("No active account configured.") name = active summaries = {s.name: s for s in cm.list_accounts()} if name not in summaries: raise ConfigError(f"Account '{name}' not found.") return summaries[name] ``` ### test ``` test(name: str | None = None) -> AccountTestResult ``` Probe `/me` for the named account and return the structured result. Resolves the named account (or active account when `name` is None), constructs a short-lived :class:`MixpanelAPIClient` against `/me`, and reports whether the credentials are accepted plus the authenticated user identity / accessible-project count from the response body. Never raises β€” every failure mode (account not found, missing credentials, OAuth refresh failure, HTTP error) is captured in `result.error` so the CLI can render a structured failure message and downstream tooling can color accounts as `needs_login` / `needs_token` based on the error string. | PARAMETER | DESCRIPTION | | --------- | --------------------------------------------------------------- | | `name` | Account to test; None means the active account. **TYPE:** \`str | | RETURNS | DESCRIPTION | | ------------------- | ---------------------------------------------------------- | | `AccountTestResult` | class:AccountTestResult β€” ok=True with user populated | | `AccountTestResult` | on success, or ok=False with error describing the failure. | Source code in `src/mixpanel_headless/accounts.py` ``` def test(name: str | None = None) -> AccountTestResult: """Probe ``/me`` for the named account and return the structured result. Resolves the named account (or active account when ``name`` is None), constructs a short-lived :class:`MixpanelAPIClient` against ``/me``, and reports whether the credentials are accepted plus the authenticated user identity / accessible-project count from the response body. Never raises β€” every failure mode (account not found, missing credentials, OAuth refresh failure, HTTP error) is captured in ``result.error`` so the CLI can render a structured failure message and downstream tooling can color accounts as ``needs_login`` / ``needs_token`` based on the error string. Args: name: Account to test; ``None`` means the active account. Returns: :class:`AccountTestResult` β€” ``ok=True`` with ``user`` populated on success, or ``ok=False`` with ``error`` describing the failure. """ try: summary = show(name) except ConfigError as exc: return AccountTestResult( account_name=name or "(none)", ok=False, error=str(exc) ) cm = _config() try: account = cm.get_account(summary.name) except ConfigError as exc: # pragma: no cover β€” show() already validated return AccountTestResult(account_name=summary.name, ok=False, error=str(exc)) # Lazy imports to keep import-time cheap (httpx + threading pull in lots). from mixpanel_headless._internal.api_client import MixpanelAPIClient from mixpanel_headless._internal.auth.session import Project, Session from mixpanel_headless._internal.me import MeResponse # ``MixpanelAPIClient`` requires a project to construct a Session even # though ``/me`` itself is project-agnostic. Use the account's default # when present, falling back to ``"0"`` so probes still work for fresh # ``oauth_browser`` accounts that have not yet been login'd. placeholder_project = account.default_project or "0" probe_session = Session( account=account, project=Project(id=placeholder_project), ) api_client = MixpanelAPIClient(session=probe_session) try: try: me_raw = api_client.me() except Exception as exc: # noqa: BLE001 β€” capture every failure mode # Preserve the structured error context when the underlying # failure was a library exception. Plain ``str(exc)`` loses # the machine-readable code that downstream tooling # (auth_manager.py, JSON consumers) uses to color accounts # as needs_login / needs_token / etc. return _build_test_failure_result(summary.name, "/me probe failed", exc) try: me_resp = MeResponse.model_validate(me_raw) except Exception as exc: # noqa: BLE001 β€” malformed payload return _build_test_failure_result( summary.name, "/me response could not be parsed", exc ) user: MeUserInfo | None = None if me_resp.user_id is not None and me_resp.user_email is not None: user = MeUserInfo(id=me_resp.user_id, email=me_resp.user_email) project_count = len(me_resp.projects) if me_resp.projects else 0 return AccountTestResult( account_name=summary.name, ok=True, user=user, accessible_project_count=project_count, ) finally: api_client.close() ``` ### login ``` login(name: str, *, open_browser: bool = True) -> OAuthLoginResult ``` Run the OAuth browser flow for an `oauth_browser` account. Drives the full PKCE login dance: 1. Validate `name` resolves to an `oauth_browser` account. 1. Run :meth:`OAuthFlow.login` (PKCE + browser callback + token exchange). 1. Persist the resulting tokens atomically to `~/.mp/accounts/{name}/tokens.json`. 1. Probe `/me` to capture the authenticated user identity and (when missing) backfill `account.default_project` with the first accessible project. The browser is opened by default; pass `open_browser=False` to skip the call (useful for headless environments where the user copies the authorization URL manually). | PARAMETER | DESCRIPTION | | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `name` | Account name (must be oauth_browser type). **TYPE:** `str` | | `open_browser` | Whether to launch the system browser. When False, the authorize URL is printed to stderr for manual copy (CLI flag: mp account login NAME --no-browser). **TYPE:** `bool` **DEFAULT:** `True` | | RETURNS | DESCRIPTION | | ------------------ | ------------------------------------------------------------------------------------- | | `An` | class:OAuthLoginResult describing the persistence paths, **TYPE:** `OAuthLoginResult` | | `OAuthLoginResult` | token expiry, and (best-effort) authenticated user identity. | | RAISES | DESCRIPTION | | ------------- | --------------------------------------------------------------------------------- | | `ConfigError` | name is not configured or is not oauth_browser. | | `OAuthError` | Any leg of the PKCE flow fails (registration, browser, callback, token exchange). | Source code in `src/mixpanel_headless/accounts.py` ``` def login( name: str, *, open_browser: bool = True, ) -> OAuthLoginResult: """Run the OAuth browser flow for an ``oauth_browser`` account. Drives the full PKCE login dance: 1. Validate ``name`` resolves to an ``oauth_browser`` account. 2. Run :meth:`OAuthFlow.login` (PKCE + browser callback + token exchange). 3. Persist the resulting tokens atomically to ``~/.mp/accounts/{name}/tokens.json``. 4. Probe ``/me`` to capture the authenticated user identity and (when missing) backfill ``account.default_project`` with the first accessible project. The browser is opened by default; pass ``open_browser=False`` to skip the call (useful for headless environments where the user copies the authorization URL manually). Args: name: Account name (must be ``oauth_browser`` type). open_browser: Whether to launch the system browser. When False, the authorize URL is printed to stderr for manual copy (CLI flag: ``mp account login NAME --no-browser``). Returns: An :class:`OAuthLoginResult` describing the persistence paths, token expiry, and (best-effort) authenticated user identity. Raises: ConfigError: ``name`` is not configured or is not ``oauth_browser``. OAuthError: Any leg of the PKCE flow fails (registration, browser, callback, token exchange). """ cm = _config() account = cm.get_account(name) if not isinstance(account, OAuthBrowserAccount): raise ConfigError( f"`mp account login` is only valid for oauth_browser accounts; " f"'{name}' is type '{account.type}'." ) # Lazy import β€” OAuthFlow pulls in browser / threading machinery. from mixpanel_headless._internal.auth.flow import OAuthFlow flow = OAuthFlow(region=account.region) # ``persist=False`` skips the v2 ``~/.mp/oauth/tokens_{region}.json`` # write β€” v3 owns ``~/.mp/accounts/{name}/tokens.json`` exclusively. tokens = flow.login(persist=False, open_browser=open_browser) # /me probe: validates the freshly minted bearer + backfills the # account's default_project on first login. The probe runs against # the in-memory token via _FreshBrowserBearer so a cross-check # failure (e.g. user authed to us but the picked project lives in # eu) does not leave wrong-region tokens at the user-visible # ``~/.mp/accounts/{name}/tokens.json``. Tokens persist only after # validation succeeds β€” same atomic-publish discipline as # ``_login_unified_new_browser``. user: MeUserInfo | None = None chosen_project = account.default_project bearer = _FreshBrowserBearer(tokens.access_token.get_secret_value()) try: me_resp = _fetch_me(account, token_resolver=bearer) except Exception as exc: # noqa: BLE001 β€” re-raise as OAuthError below raise OAuthError( f"Login succeeded but `/me` probe failed: {exc}", code="OAUTH_TOKEN_ERROR", details={"account_name": name, "region": account.region}, ) from exc if me_resp.user_id is not None and me_resp.user_email is not None: user = MeUserInfo(id=me_resp.user_id, email=me_resp.user_email) if chosen_project is None and me_resp.projects: chosen_project = ProjectId(next(iter(sorted(me_resp.projects)))) # E-2 cross-check: the picked project must live in the same # cluster the bearer was minted against, otherwise every # subsequent request 401s with no obvious connection back to # the region choice. _assert_project_region_matches(me_resp, chosen_project, account.region) # Validation passed β€” safe to persist. Backfill default_project too # if the cross-check picked one. Both writes go to disk only now. tokens_path = _persist_browser_tokens(name, tokens) if chosen_project is not None and chosen_project != account.default_project: cm.update_account(name, default_project=chosen_project) return OAuthLoginResult( account_name=name, user=user, expires_at=tokens.expires_at, tokens_path=tokens_path, client_path=_client_info_path(account.region), ) ``` ### login_unified ``` login_unified( *, name: str | None = None, region: Region | None = None, project: str | None = None, account_type: AccountType | None = None, no_browser: bool = False, secret_stdin: bool = False, token_env: str | None = None, service_account: bool = False, project_picker: ProjectPicker | None = None, progress: ProgressFactory | None = None, ) -> AccountSummary ``` Add and activate a Mixpanel account in one orchestrated call. The conversational entry point for `mp login`. Composes the helpers landed in earlier 043 commits (region probe, name derivation, SA project relaxation) with the existing PKCE flow into a single call that goes from "no config" to "ready to query". ##### Auth-type detection priority 1. `account_type` parameter (explicit override). 1. `token_env` set β†’ `oauth_token`. 1. `MP_USERNAME` + `MP_SECRET` env both set β†’ `service_account`. 1. `MP_OAUTH_TOKEN` env set β†’ `oauth_token`. 1. Default β†’ `oauth_browser`. ##### Project-selection priority (applied AFTER `/me`) 1. `project` parameter (must exist in `/me`). 1. `MP_PROJECT_ID` env (warn-and-fall-through if missing from `/me`). 1. Single project in `/me` β†’ auto-pick. 1. Caller-supplied `project_picker` callback (CLI provides one; library raises `ConfigError` E-8 when no callback is supplied). ##### Region resolution - `oauth_browser`: `region` (default `"us"`) committed before the PKCE redirect; cross-checked against the picked project's `domain` after the callback. - `service_account` / `oauth_token`: when `region is None`, probes `us β†’ eu β†’ in` against `/me` until first 200. ##### Re-login (when an existing account matches the resolved name) - Refreshes tokens (oauth_browser) or updates credentials (SA / token). - `default_project` is preserved; `project` / `MP_PROJECT_ID` are ignored on this path (E-5 informational stderr note). - Region change β†’ refused (E-3). - Auth-type change β†’ refused (E-4). ##### Output Progress narration (region-probe attempts, the E-5 re-login note) is written to `stderr` via :func:`_narrate`. Library callers who want it suppressed can redirect the parent process's `stderr`; the function does not currently expose a programmatic `narrate=False` toggle. | PARAMETER | DESCRIPTION | | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `name` | Explicit local account name. Wins over derived names. **TYPE:** \`str | | `region` | Explicit region. None triggers the probe (SA / token) or defaults to us (oauth_browser). **TYPE:** \`Region | | `project` | Explicit project ID. Must exist in /me. **TYPE:** \`str | | `account_type` | Explicit auth-type override. **TYPE:** \`AccountType | | `no_browser` | For oauth_browser, print the authorize URL instead of launching the browser. Combined with a non-browser account_type raises :class:InvalidArgumentError (violation="no_browser_misuse"). **TYPE:** `bool` **DEFAULT:** `False` | | `secret_stdin` | For service_account, read the secret from stdin. Combined with a non-SA account_type raises :class:InvalidArgumentError (violation="secret_stdin_misuse"). **TYPE:** `bool` **DEFAULT:** `False` | | `token_env` | For oauth_token, env-var name carrying the bearer. Defaults to MP_OAUTH_TOKEN when not set. **TYPE:** \`str | | `service_account` | When True, forces account_type = "service_account" (mirrors the CLI --service-account flag). Combined with token_env raises :class:InvalidArgumentError (violation="mutually_exclusive"). Library callers can instead pass account_type="service_account" directly; this flag exists so the CLI can forward its raw arguments without per-flag remapping. **TYPE:** `bool` **DEFAULT:** `False` | | `project_picker` | Callable invoked with (MeResponse, sorted_projects) when len(me.projects) > 1 and no other project source resolves. Returns the chosen project ID. The CLI supplies a TTY-aware picker; library callers can supply their own or leave it None to fail-fast non-interactively. **TYPE:** \`ProjectPicker | | `progress` | Optional CM factory wrapped around the /me round- trip. The CLI passes a Rich-spinner-backed factory so the terminal does not appear hung while /me runs. Library callers leave None and the orchestrator substitutes :class:contextlib.nullcontext. **TYPE:** \`ProgressFactory | | RETURNS | DESCRIPTION | | ---------------- | ------------------------------------------------------- | | `AccountSummary` | class:AccountSummary for the newly added (or refreshed) | | `AccountSummary` | account, with user_email / project_id / project_name | | `AccountSummary` | populated from the /me lookup. | | RAISES | DESCRIPTION | | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `InvalidArgumentError` | Mutually-incompatible flag combinations (service_account + token_env; no_browser against non-browser; secret_stdin against non-SA). Carries a violation discriminator and detected_auth_type in details. Maps to CLI exit 3. | | `ConfigError` | Project not visible (E-6), region mismatch (E-2 / E-3), type mismatch (E-4), missing required env (cred collection), or non-interactive context with no project / org default (E-8 / E-9). | | `AccountExistsError` | Derived account name collides with an existing account (browser flow); pass name= to disambiguate. | | `ProjectNotFoundError` | Explicit project= not visible to /me. | | `OAuthError` | PKCE failure or all-region probe failure (raised as :class:RegionProbeError subclass). | | `RegionProbeNetworkError` | All probe attempts failed at the network layer (subclass of :class:RegionProbeError). | Example ``` # Browser login, single project, derived name from /me org result = login_unified() # AccountSummary(name="acme-corp", type="oauth_browser", ...) # Service account from env, region auto-detected os.environ["MP_USERNAME"] = "svc" os.environ["MP_SECRET"] = "..." result = login_unified() # detects SA, probes region # Re-login: refresh tokens for an existing account result = login_unified(name="acme-corp") ``` Source code in `src/mixpanel_headless/accounts.py` ```` def login_unified( *, name: str | None = None, region: Region | None = None, project: str | None = None, account_type: AccountType | None = None, no_browser: bool = False, secret_stdin: bool = False, token_env: str | None = None, service_account: bool = False, project_picker: ProjectPicker | None = None, progress: ProgressFactory | None = None, ) -> AccountSummary: """Add and activate a Mixpanel account in one orchestrated call. The conversational entry point for ``mp login``. Composes the helpers landed in earlier 043 commits (region probe, name derivation, SA project relaxation) with the existing PKCE flow into a single call that goes from "no config" to "ready to query". ## Auth-type detection priority 1. ``account_type`` parameter (explicit override). 2. ``token_env`` set β†’ ``oauth_token``. 3. ``MP_USERNAME`` + ``MP_SECRET`` env both set β†’ ``service_account``. 4. ``MP_OAUTH_TOKEN`` env set β†’ ``oauth_token``. 5. Default β†’ ``oauth_browser``. ## Project-selection priority (applied AFTER ``/me``) 1. ``project`` parameter (must exist in ``/me``). 2. ``MP_PROJECT_ID`` env (warn-and-fall-through if missing from ``/me``). 3. Single project in ``/me`` β†’ auto-pick. 4. Caller-supplied ``project_picker`` callback (CLI provides one; library raises ``ConfigError`` E-8 when no callback is supplied). ## Region resolution - ``oauth_browser``: ``region`` (default ``"us"``) committed before the PKCE redirect; cross-checked against the picked project's ``domain`` after the callback. - ``service_account`` / ``oauth_token``: when ``region is None``, probes ``us β†’ eu β†’ in`` against ``/me`` until first 200. ## Re-login (when an existing account matches the resolved name) - Refreshes tokens (oauth_browser) or updates credentials (SA / token). - ``default_project`` is preserved; ``project`` / ``MP_PROJECT_ID`` are ignored on this path (E-5 informational stderr note). - Region change β†’ refused (E-3). - Auth-type change β†’ refused (E-4). ## Output Progress narration (region-probe attempts, the E-5 re-login note) is written to ``stderr`` via :func:`_narrate`. Library callers who want it suppressed can redirect the parent process's ``stderr``; the function does not currently expose a programmatic ``narrate=False`` toggle. Args: name: Explicit local account name. Wins over derived names. region: Explicit region. ``None`` triggers the probe (SA / token) or defaults to ``us`` (oauth_browser). project: Explicit project ID. Must exist in ``/me``. account_type: Explicit auth-type override. no_browser: For oauth_browser, print the authorize URL instead of launching the browser. Combined with a non-browser ``account_type`` raises :class:`InvalidArgumentError` (``violation="no_browser_misuse"``). secret_stdin: For service_account, read the secret from stdin. Combined with a non-SA ``account_type`` raises :class:`InvalidArgumentError` (``violation="secret_stdin_misuse"``). token_env: For oauth_token, env-var name carrying the bearer. Defaults to ``MP_OAUTH_TOKEN`` when not set. service_account: When ``True``, forces ``account_type = "service_account"`` (mirrors the CLI ``--service-account`` flag). Combined with ``token_env`` raises :class:`InvalidArgumentError` (``violation="mutually_exclusive"``). Library callers can instead pass ``account_type="service_account"`` directly; this flag exists so the CLI can forward its raw arguments without per-flag remapping. project_picker: Callable invoked with ``(MeResponse, sorted_projects)`` when ``len(me.projects) > 1`` and no other project source resolves. Returns the chosen project ID. The CLI supplies a TTY-aware picker; library callers can supply their own or leave it ``None`` to fail-fast non-interactively. progress: Optional CM factory wrapped around the ``/me`` round- trip. The CLI passes a Rich-spinner-backed factory so the terminal does not appear hung while ``/me`` runs. Library callers leave ``None`` and the orchestrator substitutes :class:`contextlib.nullcontext`. Returns: :class:`AccountSummary` for the newly added (or refreshed) account, with ``user_email`` / ``project_id`` / ``project_name`` populated from the ``/me`` lookup. Raises: InvalidArgumentError: Mutually-incompatible flag combinations (``service_account`` + ``token_env``; ``no_browser`` against non-browser; ``secret_stdin`` against non-SA). Carries a ``violation`` discriminator and ``detected_auth_type`` in ``details``. Maps to CLI exit 3. ConfigError: Project not visible (E-6), region mismatch (E-2 / E-3), type mismatch (E-4), missing required env (cred collection), or non-interactive context with no project / org default (E-8 / E-9). AccountExistsError: Derived account name collides with an existing account (browser flow); pass ``name=`` to disambiguate. ProjectNotFoundError: Explicit ``project=`` not visible to ``/me``. OAuthError: PKCE failure or all-region probe failure (raised as :class:`RegionProbeError` subclass). RegionProbeNetworkError: All probe attempts failed at the network layer (subclass of :class:`RegionProbeError`). Example: ```python # Browser login, single project, derived name from /me org result = login_unified() # AccountSummary(name="acme-corp", type="oauth_browser", ...) # Service account from env, region auto-detected os.environ["MP_USERNAME"] = "svc" os.environ["MP_SECRET"] = "..." result = login_unified() # detects SA, probes region # Re-login: refresh tokens for an existing account result = login_unified(name="acme-corp") ``` """ # Fold the CLI's --service-account flag into the explicit # ``account_type`` parameter so flag-combination validation has a # single source of truth. Library callers that pass # ``account_type="service_account"`` directly can leave # ``service_account=False``; both spellings produce identical # downstream behavior. if service_account: if account_type is not None and account_type != "service_account": # Two explicit but conflicting account-type signals β€” treat # as mutually-exclusive misuse. raise InvalidArgumentError( f"--service-account conflicts with explicit account_type=" f"{account_type!r}.", violation="mutually_exclusive", detected_auth_type=account_type, ) if token_env is not None: raise InvalidArgumentError( "--service-account and --token-env are mutually exclusive.\n\n" "Pick one auth type:\n" " mp login --service-account\n" " mp login --token-env MY_OAUTH_TOKEN_VAR", violation="mutually_exclusive", detected_auth_type="service_account", ) account_type = "service_account" detected_type = _detect_login_type(account_type, token_env) # Per-flag misuse: --no-browser is meaningful only for oauth_browser; # --secret-stdin is meaningful only for service_account. Surface these # before any I/O so callers fail fast. if no_browser and detected_type != "oauth_browser": raise InvalidArgumentError( f"--no-browser is only meaningful for the oauth_browser auth " f"type.\n\nDetected auth type: {detected_type}.", violation="no_browser_misuse", detected_auth_type=detected_type, ) if secret_stdin and detected_type != "service_account": raise InvalidArgumentError( f"--secret-stdin is only meaningful for the service_account " f"auth type.\n\nDetected auth type: {detected_type}.", violation="secret_stdin_misuse", detected_auth_type=detected_type, ) # Default progress to nullcontext so library callers (Cowork, scripts, # tests) do not have to thread a CM through every invocation. The CLI # passes a Rich-spinner factory; everyone else gets the no-op. if progress is None: progress = lambda _msg: contextlib.nullcontext() # noqa: E731 # Re-login path: when name is explicit AND the account already exists, # refresh credentials and bail before the new-account machinery runs. cm = _config() if name is not None: try: existing = cm.get_account(name) except ConfigError: existing = None if existing is not None: summary = _login_unified_relogin( cm, existing=existing, requested_type=detected_type, requested_region=region, project=project, no_browser=no_browser, secret_stdin=secret_stdin, token_env=token_env, progress=progress, ) else: summary = _login_unified_new( cm, detected_type=detected_type, name=name, region=region, project=project, no_browser=no_browser, secret_stdin=secret_stdin, token_env=token_env, project_picker=project_picker, progress=progress, ) else: summary = _login_unified_new( cm, detected_type=detected_type, name=None, region=region, project=project, no_browser=no_browser, secret_stdin=secret_stdin, token_env=token_env, project_picker=project_picker, progress=progress, ) # Activate the (new or refreshed) account so library callers β€” not # just the CLI β€” see the documented "Add and activate" semantics. # ``add()`` only auto-activates the FIRST account; subsequent adds # and the relogin path leave ``[active].account`` untouched without # this explicit promotion. A single ``_mutate()`` transaction keeps # the next process from observing a half-swapped state. use(summary.name) return summary ```` ### logout ``` logout(name: str) -> None ``` Remove the on-disk OAuth tokens for an `oauth_browser` account. | PARAMETER | DESCRIPTION | | --------- | ----------------------------- | | `name` | Account name. **TYPE:** `str` | | RAISES | DESCRIPTION | | ------------- | ------------------ | | `ConfigError` | Account not found. | Source code in `src/mixpanel_headless/accounts.py` ``` def logout(name: str) -> None: """Remove the on-disk OAuth tokens for an ``oauth_browser`` account. Args: name: Account name. Raises: ConfigError: Account not found. """ summary = show(name) # raises if missing tokens_path = account_dir(summary.name) / "tokens.json" if tokens_path.exists(): tokens_path.unlink() ``` ### token ``` token(name: str | None = None) -> str | None ``` Return the current bearer token for an OAuth account. | PARAMETER | DESCRIPTION | | --------- | ------------------------------------------------------------ | | `name` | Account name; None means the active account. **TYPE:** \`str | | RETURNS | DESCRIPTION | | ------- | ----------- | | \`str | None\` | | \`str | None\` | | \`str | None\` | | \`str | None\` | | RAISES | DESCRIPTION | | ------------- | ----------------------------------------------------------------------- | | `ConfigError` | Account not found. | | `OAuthError` | OAuth token cannot be resolved (missing tokens, missing env var, etc.). | Source code in `src/mixpanel_headless/accounts.py` ``` def token(name: str | None = None) -> str | None: """Return the current bearer token for an OAuth account. Args: name: Account name; ``None`` means the active account. Returns: For ``service_account``: ``None`` (no bearer). For ``oauth_browser``: the on-disk access token (raises ``OAuthError`` via the resolver if unavailable). For ``oauth_token``: the inline / env-resolved token. Raises: ConfigError: Account not found. OAuthError: OAuth token cannot be resolved (missing tokens, missing env var, etc.). """ cm = _config() summary = show(name) account = cm.get_account(summary.name) resolver = OnDiskTokenResolver() if isinstance(account, ServiceAccount): return None if isinstance(account, OAuthBrowserAccount): return resolver.get_browser_token(account.name, account.region) if isinstance(account, OAuthTokenAccount): return resolver.get_static_token(account) raise ConfigError( # pragma: no cover β€” Literal exhaustiveness f"Unknown account type for {summary.name!r}" ) ``` ### export_bridge ``` export_bridge( *, to: Path, account: str | None = None, project: str | None = None, workspace: int | None = None, ) -> Path ``` Export the named (or active) account as a v2 bridge file. Resolves the account, attaches any `[settings].custom_header` as `bridge.headers` (B5 β€” header attaches in memory at resolution time for the consumer), and writes a 0o600 file at `to` via :func:`bridge.export_bridge`. | PARAMETER | DESCRIPTION | | ----------- | ------------------------------------------------------------------- | | `to` | Destination path for the bridge file. **TYPE:** `Path` | | `account` | Account to export; None means the active account. **TYPE:** \`str | | `project` | Optional pinned project ID. None omits the field. **TYPE:** \`str | | `workspace` | Optional pinned workspace ID. None omits the field. **TYPE:** \`int | | RETURNS | DESCRIPTION | | ------- | --------------------------------------- | | `Path` | The path that was written (same as to). | | RAISES | DESCRIPTION | | ------------- | ----------------------------------------------------------------------- | | `ConfigError` | Account not found, no active account, or BridgeFile validation failure. | | `OAuthError` | account.type == "oauth_browser" but no on-disk tokens are available. | Source code in `src/mixpanel_headless/accounts.py` ``` def export_bridge( *, to: Path, account: str | None = None, project: str | None = None, workspace: int | None = None, ) -> Path: """Export the named (or active) account as a v2 bridge file. Resolves the account, attaches any ``[settings].custom_header`` as ``bridge.headers`` (B5 β€” header attaches in memory at resolution time for the consumer), and writes a 0o600 file at ``to`` via :func:`bridge.export_bridge`. Args: to: Destination path for the bridge file. account: Account to export; ``None`` means the active account. project: Optional pinned project ID. ``None`` omits the field. workspace: Optional pinned workspace ID. ``None`` omits the field. Returns: The path that was written (same as ``to``). Raises: ConfigError: Account not found, no active account, or ``BridgeFile`` validation failure. OAuthError: ``account.type == "oauth_browser"`` but no on-disk tokens are available. """ from mixpanel_headless._internal.auth.bridge import ( export_bridge as _bridge_export, ) cm = _config() name = account or cm.get_active().account if name is None: raise ConfigError("No account specified and no active account configured.") acct = cm.get_account(name) header = cm.get_custom_header() headers = {header[0]: header[1]} if header is not None else None return _bridge_export( acct, to=to, project=project, workspace=workspace, headers=headers, token_resolver=OnDiskTokenResolver(), ) ``` ### remove_bridge ``` remove_bridge(*, at: Path | None = None) -> bool ``` Remove the v2 bridge file at `at` (or the default path). | PARAMETER | DESCRIPTION | | --------- | ----------------------------------------------------------------------------------------- | | `at` | Bridge file path; None means MP_AUTH_FILE then the default search paths. **TYPE:** \`Path | | RETURNS | DESCRIPTION | | ------- | ---------------------------------------------------- | | `bool` | True if a file was deleted; False if none was found. | Source code in `src/mixpanel_headless/accounts.py` ``` def remove_bridge(*, at: Path | None = None) -> bool: """Remove the v2 bridge file at ``at`` (or the default path). Args: at: Bridge file path; ``None`` means ``MP_AUTH_FILE`` then the default search paths. Returns: ``True`` if a file was deleted; ``False`` if none was found. """ from mixpanel_headless._internal.auth.bridge import ( remove_bridge as _bridge_remove, ) return _bridge_remove(at=at) ``` #### Frictionless login (`login_unified`) Composes auth-type detection, region resolution, `/me` lookup, project picker, and account-name derivation into one call. Backs the CLI's `mp login` command. ``` import mixpanel_headless as mp # Browser PKCE β€” derives region, name, project from /me. summary = mp.accounts.login_unified() print(summary.user_email, summary.project_id, summary.project_name) # Service account from env, region auto-probed (us β†’ eu β†’ in): import os os.environ["MP_USERNAME"] = "sa_xxx" os.environ["MP_SECRET"] = "..." summary = mp.accounts.login_unified() # Re-login: refresh tokens for an existing account. summary = mp.accounts.login_unified(name="acme-corp") # Multi-project β€” supply a picker callback for non-CLI contexts. def picker(me, sorted_projects): """Return the project_id you want to bind.""" return sorted_projects[0][0] summary = mp.accounts.login_unified(project_picker=picker) ``` Auth-type detection ladder (priority order): 1. Explicit `account_type=` (or the CLI's `--service-account` / `--token-env`). 1. `MP_USERNAME` + `MP_SECRET` set β†’ `service_account`. 1. `MP_OAUTH_TOKEN` set β†’ `oauth_token`. 1. Otherwise β†’ `oauth_browser` (PKCE). Region behavior is auth-type-specific. `service_account` and `oauth_token` paths probe `us β†’ eu β†’ in` against `/me` when `region=` is not passed, returning the first 200. `oauth_browser` commits to the supplied `region` (or defaults to `"us"`) before the PKCE redirect, then cross-checks the picked project's domain after the callback. EU and India browser users must pass `region="eu"` or `region="in"` explicitly. Raises `RegionProbeError` / `RegionProbeNetworkError` if no region accepts the credential (SA / token paths only), `InvalidArgumentError` for mutually-incompatible flag combinations, `ProjectNotFoundError` for an explicit `project=` not visible to `/me`, and `AccountExistsError` when the derived name collides on the browser path. See [Exceptions](https://mixpanel.github.io/mixpanel-headless/api/exceptions/#oauth-exceptions) for the full set. ### `mp.session` Read and write the persisted `[active]` block. ## mixpanel_headless.session Public `mp.session` namespace. Thin wrapper around :class:`~mixpanel_headless._internal.config.ConfigManager` exposing the persisted `[active]` session and per-axis updates. Note: this module shadows the :class:`Session` value type. Public callers access via `import mixpanel_headless; mp.session.show()` (module) or `import mixpanel_headless; mp.Session(...)` (the type). Reference: specs/042-auth-architecture-redesign/contracts/python-api.md Β§7. ### show ``` show() -> ActiveSession ``` Return the persisted `[active]` block. | RETURNS | DESCRIPTION | | --------------- | -------------------------------------------------------- | | `ActiveSession` | ActiveSession with account and workspace (each may be | | `ActiveSession` | None). Project lives on the active account as | | `ActiveSession` | account.default_project β€” to read it, fetch the account. | Source code in `src/mixpanel_headless/session.py` ``` def show() -> ActiveSession: """Return the persisted ``[active]`` block. Returns: ``ActiveSession`` with ``account`` and ``workspace`` (each may be None). Project lives on the active account as ``account.default_project`` β€” to read it, fetch the account. """ return _config().get_active() ``` ### use ``` use( *, account: str | None = None, project: str | None = None, workspace: int | None = None, target: str | None = None, ) -> None ``` Update one or more axes in the persisted config. `account=` and `workspace=` are written to `[active]`. `project=` is written to the **active account's** `default_project` (project lives on the account, not in `[active]`). `target=` is mutually exclusive with the per-axis kwargs and applies all three axes atomically (writing project to the target account's `default_project`). All updates land in a single `apply_session` transaction so the on-disk state never reflects a partial swap (e.g., new account but stale project). | PARAMETER | DESCRIPTION | | ----------- | --------------------------------------------------------------------- | | `account` | New active account name. **TYPE:** \`str | | `project` | New project ID (digit string) for the active account. **TYPE:** \`str | | `workspace` | New active workspace ID. **TYPE:** \`int | | `target` | Apply this target's three axes atomically. **TYPE:** \`str | | RAISES | DESCRIPTION | | ------------- | ----------------------------------------------------------------------------------------------- | | `ValueError` | target= combined with any axis kwarg. | | `ConfigError` | Referenced account or target not found, or project= supplied with no active account configured. | Source code in `src/mixpanel_headless/session.py` ``` def use( *, account: str | None = None, project: str | None = None, workspace: int | None = None, target: str | None = None, ) -> None: """Update one or more axes in the persisted config. ``account=`` and ``workspace=`` are written to ``[active]``. ``project=`` is written to the **active account's** ``default_project`` (project lives on the account, not in ``[active]``). ``target=`` is mutually exclusive with the per-axis kwargs and applies all three axes atomically (writing project to the target account's ``default_project``). All updates land in a single ``apply_session`` transaction so the on-disk state never reflects a partial swap (e.g., new account but stale project). Args: account: New active account name. project: New project ID (digit string) for the active account. workspace: New active workspace ID. target: Apply this target's three axes atomically. Raises: ValueError: ``target=`` combined with any axis kwarg. ConfigError: Referenced account or target not found, or ``project=`` supplied with no active account configured. """ if target is not None and ( account is not None or project is not None or workspace is not None ): raise ValueError( "`target=` is mutually exclusive with `account=`/`project=`/`workspace=`." ) cm = _config() if target is not None: cm.apply_target(target) return cm.apply_session(account=account, project=project, workspace=workspace) ``` ### `mp.targets` Manage saved (account, project, optional workspace) cursor positions. ## mixpanel_headless.targets Public `mp.targets` namespace. Thin wrapper around :class:`~mixpanel_headless._internal.config.ConfigManager` exposing target CRUD and activation. Targets are saved (account, project, workspace?) triples used as named cursor positions: `mp.targets.use("ecom")` writes all three axes to `[active]` in a single config save. Reference: specs/042-auth-architecture-redesign/contracts/python-api.md Β§6. ### list ``` list() -> builtins.list[Target] ``` Return all configured targets sorted by name. | RETURNS | DESCRIPTION | | -------------- | ------------------------------------- | | `list[Target]` | Sorted list of :class:Target records. | Source code in `src/mixpanel_headless/targets.py` ``` def list() -> builtins.list[Target]: # noqa: A001 β€” public namespace shadow """Return all configured targets sorted by name. Returns: Sorted list of :class:`Target` records. """ return _config().list_targets() ``` ### add ``` add( name: str, *, account: str, project: str, workspace: int | None = None ) -> Target ``` Add a new target block. | PARAMETER | DESCRIPTION | | ----------- | ----------------------------------------------------- | | `name` | Target name (block key). **TYPE:** `str` | | `account` | Referenced account name (must exist). **TYPE:** `str` | | `project` | Project ID (digit string). **TYPE:** `str` | | `workspace` | Optional workspace ID. **TYPE:** \`int | | RETURNS | DESCRIPTION | | -------- | ------------------------------ | | `Target` | The constructed :class:Target. | | RAISES | DESCRIPTION | | ------------- | ------------------------------------------------------- | | `ConfigError` | Duplicate name, missing account, or validation failure. | Source code in `src/mixpanel_headless/targets.py` ``` def add( name: str, *, account: str, project: str, workspace: int | None = None, ) -> Target: """Add a new target block. Args: name: Target name (block key). account: Referenced account name (must exist). project: Project ID (digit string). workspace: Optional workspace ID. Returns: The constructed :class:`Target`. Raises: ConfigError: Duplicate name, missing account, or validation failure. """ return _config().add_target( name, account=account, project=project, workspace=workspace ) ``` ### remove ``` remove(name: str) -> None ``` Remove a target block. | PARAMETER | DESCRIPTION | | --------- | --------------------------------- | | `name` | Target to remove. **TYPE:** `str` | | RAISES | DESCRIPTION | | ------------- | ---------------------- | | `ConfigError` | Target does not exist. | Source code in `src/mixpanel_headless/targets.py` ``` def remove(name: str) -> None: """Remove a target block. Args: name: Target to remove. Raises: ConfigError: Target does not exist. """ _config().remove_target(name) ``` ### use ``` use(name: str) -> None ``` Apply the target β€” write all three axes to `[active]` atomically. | PARAMETER | DESCRIPTION | | --------- | -------------------------------- | | `name` | Target to apply. **TYPE:** `str` | | RAISES | DESCRIPTION | | ------------- | -------------------------------------------------------- | | `ConfigError` | Target does not exist OR its referenced account is gone. | Source code in `src/mixpanel_headless/targets.py` ``` def use(name: str) -> None: """Apply the target β€” write all three axes to ``[active]`` atomically. Args: name: Target to apply. Raises: ConfigError: Target does not exist OR its referenced account is gone. """ _config().apply_target(name) ``` ### show ``` show(name: str) -> Target ``` Return the named :class:`Target`. | PARAMETER | DESCRIPTION | | --------- | ---------------------------- | | `name` | Target name. **TYPE:** `str` | | RETURNS | DESCRIPTION | | -------- | ------------------ | | `Target` | The Target record. | | RAISES | DESCRIPTION | | ------------- | ---------------------- | | `ConfigError` | Target does not exist. | Source code in `src/mixpanel_headless/targets.py` ``` def show(name: str) -> Target: """Return the named :class:`Target`. Args: name: Target name. Returns: The Target record. Raises: ConfigError: Target does not exist. """ return _config().get_target(name) ``` ## Result Types Read-only structured results returned from the namespaces above. ### AccountSummary ## mixpanel_headless.AccountSummary Bases: `BaseModel` Read-only summary of a configured account for `mp account list`. Fields are derived from the persisted `[accounts.NAME]` block plus runtime context (`is_active`, `referenced_by_targets`). Status reflects the most recent `mp account test` outcome β€” `"untested"` is the default for accounts that have never been tested in this session. Example ``` summary = AccountSummary( name="team", type="service_account", region="us", status="ok", is_active=True, ) ``` ### name ``` name: str ``` Local config name (matches the TOML block key). ### type ``` type: AccountType ``` Discriminator value of the underlying `Account` variant. ### region ``` region: Region ``` Mixpanel region β€” `us`, `eu`, or `in`. ### status ``` status: Literal['ok', 'needs_login', 'needs_token', 'untested'] = 'untested' ``` Result of the most recent `mp account test` (or `"untested"`). ### is_active ``` is_active: bool = False ``` `True` if `[active].account == name`. ### referenced_by_targets ``` referenced_by_targets: list[str] = Field(default_factory=list) ``` Names of targets that reference this account. ### user_email ``` user_email: str | None = None ``` Authenticated user email, populated by `login_unified()` from `/me`. Persisted in the per-account `MeCache` (not in `config.toml`), so it survives across processes once login has run. `None` when the account was added via `mp account add` (no `/me` round-trip) or when `/me` did not return a `user_email`. ### project_id ``` project_id: str | None = None ``` Project ID resolved at login time. Mirror of the persisted `default_project` for convenience β€” exposed on `AccountSummary` so the `mp login` success line can render `Logged in as ... β†’ ... Β· {project_name}` without a second `ConfigManager` round-trip. `None` when no default project is set. ### project_name ``` project_name: str | None = None ``` Human-readable project name from `/me` for the resolved project. Populated alongside `project_id` by `login_unified()`. `None` when no project is configured or the project is not in `/me`. ### AccountTestResult ## mixpanel_headless.AccountTestResult Bases: `BaseModel` Outcome of `mp account test NAME` β€” captures the `/me` probe. Never raises β€” error context is captured in `error` so the CLI can print structured failure messages and `mp account list` can color accounts as `needs_login` / `needs_token` based on the error code. The `ok`/`error` fields are paired by an invariant: `ok=True` iff `error is None`. Constructing the model with both `ok=True` and a non-empty `error` (or `ok=False` and `error=None`) raises :class:`pydantic.ValidationError` to prevent ambiguous result states that would force callers to guess the right field to read. When the underlying failure is a :class:`MixpanelHeadlessError`, `error_code` and `error_details` carry the structured fields so downstream callers (the plugin's `auth_manager.py`, JSON consumers) can dispatch on the code instead of parsing the `error` message string. Both default to `None` for the success path and for failures captured from a non-library exception (network OSError, programming bug, etc.). ### account_name ``` account_name: str ``` Account that was tested. ### ok ``` ok: bool ``` `True` if the `/me` request succeeded with valid credentials. ### user ``` user: MeUserInfo | None = None ``` Authenticated principal identity, when `ok` is `True`. ### accessible_project_count ``` accessible_project_count: int | None = None ``` Number of projects the account can read from `/me`. ### error ``` error: str | None = None ``` Human-readable failure reason when `ok` is `False`. ### error_code ``` error_code: str | None = None ``` Machine-readable error code (only set when the cause was a `MixpanelHeadlessError`). ### error_details ``` error_details: dict[str, Any] | None = None ``` Structured `details` payload from the underlying `MixpanelHeadlessError`, if any. ### OAuthLoginResult ## mixpanel_headless.OAuthLoginResult Bases: `BaseModel` Outcome of `mp.accounts.login(name)` β€” captures the PKCE flow result. Returned after a successful OAuth browser flow. `user` is populated from the immediate `/me` probe issued after the token exchange so callers can confirm "you are now logged in as `alice@example.com`" without needing a follow-up call. ### account_name ``` account_name: str ``` Account that was authenticated. ### user ``` user: MeUserInfo | None = None ``` Authenticated principal identity from the post-login `/me` probe. ### expires_at ``` expires_at: datetime | None = None ``` Access-token expiry (UTC) from the token endpoint response. ### tokens_path ``` tokens_path: Path ``` Where the tokens were persisted (`~/.mp/accounts/{name}/tokens.json`). ### client_path ``` client_path: Path ``` Where the DCR client info was persisted (`~/.mp/accounts/{name}/client.json`). ### Target ## mixpanel_headless.Target Bases: `BaseModel` A saved (account, project, workspace?) triple persisted in `[targets.NAME]`. Targets are named cursor positions: `mp target use prod` writes all three axes to `[active]` in a single config save. Workspace is optional β€” when omitted, the target resolves to the project's default workspace at use time (per FR-025 lazy resolution). ### name ``` name: TargetName ``` Local target name (matches the TOML block key). ### account ``` account: AccountName ``` Local config name of the referenced account (must exist). ### project ``` project: Annotated[ProjectId, Field(min_length=1, pattern='^\\d+$')] ``` Numeric project ID (Mixpanel's wire format). ### workspace ``` workspace: Annotated[WorkspaceId, Field(gt=0)] | None = None ``` Optional workspace ID (must be a positive integer when set); `None` defers to lazy resolution. Mirrors `WorkspaceRef.id`'s `PositiveInt` constraint so bad values fail at construction rather than corrupting downstream config. ## Credential Resolution Chain When constructing a `Workspace`, each axis is resolved independently in this priority order: 1. **Environment variables** β€” the resolver reads `MP_USERNAME` + `MP_SECRET` + `MP_PROJECT_ID` + `MP_REGION` (service-account quad), `MP_OAUTH_TOKEN` + `MP_PROJECT_ID` + `MP_REGION` (OAuth-token triple), `MP_PROJECT_ID` (project axis), and `MP_WORKSPACE_ID` (workspace axis). `MP_ACCOUNT` is **not** consumed by the Python resolver β€” it only feeds the CLI's `--account` / `-a` flag via Typer's `envvar=` default. 1. **Constructor / CLI param** β€” `Workspace(account="...")`, `mp -a NAME ...`. 1. **Saved target** β€” `Workspace(target="ecom")`, `mp -t ecom ...`. 1. **Bridge file** β€” `MP_AUTH_FILE` or `~/.claude/mixpanel/auth.json`. 1. **Persisted active session** β€” the `[active]` block in `~/.mp/config.toml`. 1. **Account default** β€” `account.default_project` for the project axis. See [Configuration β†’ Credential Resolution Chain](https://mixpanel.github.io/mixpanel-headless/getting-started/configuration/#credential-resolution-chain) for examples. ## Cowork Bridge (v2) The Cowork bridge is a v2 JSON file that lets a remote VM authenticate against Mixpanel using your host machine's account and tokens. It embeds the full `Account`, optional OAuth tokens, and optional pinned project/workspace/headers. ``` from pathlib import Path import mixpanel_headless as mp # On the host mp.accounts.export_bridge(to=Path("~/.claude/mixpanel/auth.json").expanduser()) mp.accounts.remove_bridge() ``` ``` # CLI equivalents mp account export-bridge --to ~/.claude/mixpanel/auth.json mp account remove-bridge mp session --bridge # show bridge-resolved state ``` Default search order: `MP_AUTH_FILE` β†’ `~/.claude/mixpanel/auth.json` β†’ `./mixpanel_auth.json`. ## mixpanel_headless.auth_types.BridgeFile Bases: `BaseModel` Cowork credential bridge file β€” v2 schema. Embeds a full :class:`~mixpanel_headless._internal.auth.account.Account` record (with secrets inline) plus optional project / workspace pinning and a custom-headers map. Example ``` { "version": 2, "account": {"type": "oauth_browser", "name": "personal", "region": "us"}, "tokens": {"access_token": "...", "refresh_token": "...", "expires_at": "2026-04-22T12:00:00Z", "token_type": "Bearer", "scope": "read"}, "project": "3713224", "workspace": 3448413, "headers": {"X-Mixpanel-Cluster": "internal-1"} } ``` ### version ``` version: Literal[2] = 2 ``` Bridge schema version β€” always `2`. ### account ``` account: Account ``` Full Account discriminated-union record (with secrets inline by design). ### tokens ``` tokens: OAuthTokens | None = None ``` OAuth tokens β€” required iff `account.type == "oauth_browser"`. ### project ``` project: Annotated[str | None, Field(default=None, pattern='^\\d+$')] = None ``` Optional pinned project ID (numeric string). ### workspace ``` workspace: PositiveInt | None = None ``` Optional pinned workspace ID. ### headers ``` headers: dict[str, str] = Field(default_factory=dict) ``` Custom HTTP headers attached to outbound requests at resolution time. ## mixpanel_headless.auth_types.load_bridge ``` load_bridge(path: Path | None = None) -> BridgeFile | None ``` Load and validate a v2 bridge file from disk. Resolves the path in this order: 1. Argument `path` (if not None). 1. `$MP_AUTH_FILE` env var (if set). 1. Default search paths (`~/.claude/mixpanel/auth.json`, then `/mixpanel_auth.json`) β€” first existing file wins. | PARAMETER | DESCRIPTION | | --------- | ----------------------------------------------- | | `path` | Optional explicit bridge path. **TYPE:** \`Path | | RETURNS | DESCRIPTION | | ------------ | ----------- | | \`BridgeFile | None\` | | \`BridgeFile | None\` | | RAISES | DESCRIPTION | | ------------- | ----------------------------------------------------------------------- | | `ConfigError` | If a candidate file exists but is malformed or fails schema validation. | Source code in `src/mixpanel_headless/_internal/auth/bridge.py` ``` def load_bridge(path: Path | None = None) -> BridgeFile | None: """Load and validate a v2 bridge file from disk. Resolves the path in this order: 1. Argument ``path`` (if not None). 2. ``$MP_AUTH_FILE`` env var (if set). 3. Default search paths (``~/.claude/mixpanel/auth.json``, then ``/mixpanel_auth.json``) β€” first existing file wins. Args: path: Optional explicit bridge path. Returns: The parsed :class:`BridgeFile`, or ``None`` if no candidate path exists. Raises: ConfigError: If a candidate file exists but is malformed or fails schema validation. """ candidates: list[Path] = [] if path is not None: candidates.append(path) elif "MP_AUTH_FILE" in os.environ and os.environ["MP_AUTH_FILE"]: candidates.append(Path(os.environ["MP_AUTH_FILE"])) else: candidates.extend(default_bridge_search_paths()) for candidate in candidates: if not candidate.exists(): continue try: payload = json.loads(candidate.read_text(encoding="utf-8")) except (OSError, json.JSONDecodeError) as exc: raise ConfigError( f"Could not read bridge file at {candidate}: {exc}", details={"path": str(candidate)}, ) from exc try: return _bridge_adapter.validate_python(payload) except ValidationError as exc: raise ConfigError( f"Invalid bridge file at {candidate}: " f"{exc.errors(include_url=False)[0]['msg']}", details={"path": str(candidate)}, ) from exc return None ``` ## OAuth Token Plumbing Low-level types for OAuth token handling. Most users never touch these directly β€” `mp.accounts.login(name)` drives the full flow and `OnDiskTokenResolver` materializes refreshed tokens automatically. ### OAuthTokens ## mixpanel_headless.auth_types.OAuthTokens Bases: `BaseModel` Immutable OAuth 2.0 token set with expiry tracking. Stores access and optional refresh tokens along with metadata from the token response. The `is_expired` method includes a 30-second safety buffer to avoid using tokens that are about to expire. | ATTRIBUTE | DESCRIPTION | | --------------- | -------------------------------------------------------------------------------- | | `access_token` | The OAuth access token (redacted in output). **TYPE:** `SecretStr` | | `refresh_token` | The OAuth refresh token, if provided (redacted in output). **TYPE:** \`SecretStr | | `expires_at` | UTC datetime when the access token expires. **TYPE:** `datetime` | | `scope` | Space-separated list of granted scopes. **TYPE:** `str` | | `token_type` | Token type, typically "Bearer". **TYPE:** `str` | ### access_token ``` access_token: SecretStr ``` The OAuth access token (redacted in output). ### refresh_token ``` refresh_token: SecretStr | None = None ``` The OAuth refresh token, if provided (redacted in output). ### expires_at ``` expires_at: datetime ``` UTC datetime when the access token expires. Must be timezone-aware. Naive datetimes are rejected at validation time so a downstream consumer can never accidentally compare against an aware `datetime.now(timezone.utc)` and silently fall through the expiry check (Fix 25). ### scope ``` scope: str ``` Space-separated list of granted scopes. ### token_type ``` token_type: str ``` Token type, typically `'Bearer'`. ### is_expired ``` is_expired() -> bool ``` Check whether the access token is expired or about to expire. Uses a 30-second safety buffer to avoid sending tokens that are about to expire during in-flight requests. | RETURNS | DESCRIPTION | | ------- | -------------------------------------------------------------- | | `bool` | True if the token is expired or will expire within 30 seconds. | Example ``` tokens = OAuthTokens.from_token_response( {"access_token": "x", "expires_in": 10, "scope": "read", "token_type": "Bearer"} ) assert tokens.is_expired() # 10s < 30s buffer ``` Source code in `src/mixpanel_headless/_internal/auth/token.py` ```` def is_expired(self) -> bool: """Check whether the access token is expired or about to expire. Uses a 30-second safety buffer to avoid sending tokens that are about to expire during in-flight requests. Returns: True if the token is expired or will expire within 30 seconds. Example: ```python tokens = OAuthTokens.from_token_response( {"access_token": "x", "expires_in": 10, "scope": "read", "token_type": "Bearer"} ) assert tokens.is_expired() # 10s < 30s buffer ``` """ return datetime.now(timezone.utc) + timedelta(seconds=30) >= self.expires_at ```` ### from_token_response ``` from_token_response(data: dict[str, object]) -> OAuthTokens ``` Create an OAuthTokens instance from a raw token endpoint response. Computes `expires_at` by adding the `expires_in` value (in seconds) to the current UTC time. | PARAMETER | DESCRIPTION | | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `data` | Raw JSON response from the token endpoint. Must contain access_token, expires_in, scope, and token_type. May contain refresh_token. **TYPE:** `dict[str, object]` | | RETURNS | DESCRIPTION | | ------------- | ---------------------------------- | | `OAuthTokens` | A new frozen OAuthTokens instance. | | RAISES | DESCRIPTION | | ------------ | -------------------------------------------- | | `KeyError` | If required keys are missing from data. | | `ValueError` | If expires_in cannot be converted to an int. | Example ``` response = { "access_token": "eyJ...", "refresh_token": "dGhp...", "expires_in": 3600, "scope": "read:project", "token_type": "Bearer", } tokens = OAuthTokens.from_token_response(response) ``` Source code in `src/mixpanel_headless/_internal/auth/token.py` ```` @classmethod def from_token_response(cls, data: dict[str, object]) -> OAuthTokens: """Create an OAuthTokens instance from a raw token endpoint response. Computes ``expires_at`` by adding the ``expires_in`` value (in seconds) to the current UTC time. Args: data: Raw JSON response from the token endpoint. Must contain ``access_token``, ``expires_in``, ``scope``, and ``token_type``. May contain ``refresh_token``. Returns: A new frozen OAuthTokens instance. Raises: KeyError: If required keys are missing from ``data``. ValueError: If ``expires_in`` cannot be converted to an int. Example: ```python response = { "access_token": "eyJ...", "refresh_token": "dGhp...", "expires_in": 3600, "scope": "read:project", "token_type": "Bearer", } tokens = OAuthTokens.from_token_response(response) ``` """ expires_in_raw = data["expires_in"] expires_in = int(str(expires_in_raw)) expires_at = datetime.now(timezone.utc) + timedelta(seconds=expires_in) raw_refresh = data.get("refresh_token") refresh_token: SecretStr | None = None if raw_refresh is not None: refresh_token = SecretStr(str(raw_refresh)) return cls( access_token=SecretStr(str(data["access_token"])), refresh_token=refresh_token, expires_at=expires_at, scope=str(data.get("scope", "")), token_type=str(data["token_type"]), ) ```` ### OAuthClientInfo ## mixpanel_headless.auth_types.OAuthClientInfo Bases: `BaseModel` Immutable OAuth client registration metadata. Stores client information from Dynamic Client Registration (RFC 7591) for reuse across sessions without re-registering. | ATTRIBUTE | DESCRIPTION | | -------------- | -------------------------------------------------------------------------- | | `client_id` | The OAuth client identifier. **TYPE:** `str` | | `region` | Mixpanel data residency region (us, eu, or in). **TYPE:** `str` | | `redirect_uri` | The redirect URI registered with the authorization server. **TYPE:** `str` | | `scope` | Space-separated list of requested scopes. **TYPE:** `str` | | `created_at` | UTC datetime when the client was registered. **TYPE:** `datetime` | ### client_id ``` client_id: str ``` The OAuth client identifier. ### region ``` region: str ``` Mixpanel data residency region (`us`, `eu`, or `in`). ### redirect_uri ``` redirect_uri: str ``` The redirect URI registered with the authorization server. ### scope ``` scope: str ``` Space-separated list of requested scopes. ### created_at ``` created_at: datetime ``` UTC datetime when the client was registered. ### TokenResolver Protocol ## mixpanel_headless.auth_types.TokenResolver Bases: `Protocol` Protocol for producing bearer tokens for OAuth accounts. Implementations decide how to fetch (and refresh) tokens for the two OAuth account variants. Concrete implementations live in :mod:`mixpanel_headless._internal.auth.token_resolver`. ### get_browser_token ``` get_browser_token(name: str, region: Region) -> str ``` Return a fresh access token for an :class:`OAuthBrowserAccount`. | PARAMETER | DESCRIPTION | | --------- | ----------------------------------------------------------------------- | | `name` | Account name (used to locate persisted tokens on disk). **TYPE:** `str` | | `region` | Mixpanel region (used by some implementations). **TYPE:** `Region` | | RETURNS | DESCRIPTION | | ------- | -------------------------------------------- | | `str` | The current access token (no Bearer prefix). | Source code in `src/mixpanel_headless/_internal/auth/account.py` ``` def get_browser_token(self, name: str, region: Region) -> str: """Return a fresh access token for an :class:`OAuthBrowserAccount`. Args: name: Account name (used to locate persisted tokens on disk). region: Mixpanel region (used by some implementations). Returns: The current access token (no ``Bearer`` prefix). """ ... ``` ### get_static_token ``` get_static_token(account: OAuthTokenAccount) -> str ``` Return the static bearer for an :class:`OAuthTokenAccount`. | PARAMETER | DESCRIPTION | | --------- | ------------------------------------------------------------------------------ | | `account` | The account whose token or token_env to resolve. **TYPE:** `OAuthTokenAccount` | | RETURNS | DESCRIPTION | | ------- | ------------------------------------ | | `str` | The bearer token (no Bearer prefix). | Source code in `src/mixpanel_headless/_internal/auth/account.py` ``` def get_static_token(self, account: OAuthTokenAccount) -> str: """Return the static bearer for an :class:`OAuthTokenAccount`. Args: account: The account whose ``token`` or ``token_env`` to resolve. Returns: The bearer token (no ``Bearer`` prefix). """ ... ``` ### OnDiskTokenResolver ## mixpanel_headless.auth_types.OnDiskTokenResolver Bases: `TokenResolver` Default resolver: tokens live on disk per account. Reads OAuth browser tokens from `~/.mp/accounts/{name}/tokens.json` written by :class:`OAuthFlow`. Reads static tokens from either the inline `token` field on the account or the environment variable named in `token_env`. The resolver is intentionally I/O-light: the only side effects are reading files that already exist and (for expired browser tokens) refreshing via :meth:`_refresh_and_persist`, which delegates to :class:`OAuthFlow.refresh_tokens` and rewrites `~/.mp/accounts/{name}/tokens.json` atomically via `atomic_write_bytes`. All failures surface as :class:`OAuthError` so callers can give actionable error messages. ### get_browser_token ``` get_browser_token(name: str, region: Region) -> str ``` Return a fresh access token for an :class:`OAuthBrowserAccount`. Reads `~/.mp/accounts/{name}/tokens.json`, checks the recorded `expires_at` (with a 30s safety buffer), and returns the token if not expired. If expired, refreshes via :meth:`_refresh_and_persist`; raises :class:`OAuthError(code="OAUTH_REFRESH_ERROR")` if no refresh token is recorded. | PARAMETER | DESCRIPTION | | --------- | --------------------------------------------------------------------------------------------------- | | `name` | Account name (used to locate the tokens file). **TYPE:** `str` | | `region` | Mixpanel region (kept for parity with the protocol; used by some refresh paths). **TYPE:** `Region` | | RETURNS | DESCRIPTION | | ------- | -------------------------------------------- | | `str` | The current access token (no Bearer prefix). | | RAISES | DESCRIPTION | | ------------ | -------------------------------------------------------------------------------------------- | | `OAuthError` | If the tokens file is missing, malformed, expired without a refresh token, or refresh fails. | Source code in `src/mixpanel_headless/_internal/auth/token_resolver.py` ``` def get_browser_token(self, name: str, region: Region) -> str: """Return a fresh access token for an :class:`OAuthBrowserAccount`. Reads ``~/.mp/accounts/{name}/tokens.json``, checks the recorded ``expires_at`` (with a 30s safety buffer), and returns the token if not expired. If expired, refreshes via :meth:`_refresh_and_persist`; raises :class:`OAuthError(code="OAUTH_REFRESH_ERROR")` if no refresh token is recorded. Args: name: Account name (used to locate the tokens file). region: Mixpanel region (kept for parity with the protocol; used by some refresh paths). Returns: The current access token (no ``Bearer`` prefix). Raises: OAuthError: If the tokens file is missing, malformed, expired without a refresh token, or refresh fails. """ path = _account_tokens_path(name) if not path.exists(): raise OAuthError( ( f"No OAuth tokens found for account '{name}'. " f"Run `mp account login {name}` to authenticate." ), code="OAUTH_TOKEN_ERROR", details={"account_name": name, "path": str(path)}, ) try: raw = path.read_bytes() except OSError as exc: raise OAuthError( f"Could not read OAuth tokens for account '{name}' from {path}: {exc}", code="OAUTH_TOKEN_ERROR", details={"account_name": name, "path": str(path)}, ) from exc # Single source of truth for parsing β€” `OAuthTokens` enforces the # tz-aware expiry invariant and the secret-wrapping in one place. # Any drift between how tokens are written vs read is structurally # impossible because both paths now go through the same model. try: tokens = OAuthTokens.model_validate_json(raw) except ValidationError as exc: raise OAuthError( ( f"OAuth tokens for account '{name}' at {path} are malformed " f"or missing required fields. Re-run `mp account login {name}`." ), code="OAUTH_TOKEN_ERROR", details={ "account_name": name, "path": str(path), "validation_error": str(exc), }, ) from exc if tokens.is_expired(): if tokens.refresh_token is None: raise OAuthError( ( f"OAuth access token for account '{name}' has " f"expired and no refresh token is available. " f"Re-run `mp account login {name}`." ), code="OAUTH_TOKEN_ERROR", details={ "account_name": name, "region": region, "path": str(path), }, ) return self._refresh_and_persist( name=name, region=region, path=path, tokens=tokens, ) return tokens.access_token.get_secret_value() ``` ### get_static_token ``` get_static_token(account: OAuthTokenAccount) -> str ``` Return the static bearer for an :class:`OAuthTokenAccount`. Resolves the bearer from the inline `token` field if present; otherwise reads the environment variable named in `token_env`. | PARAMETER | DESCRIPTION | | --------- | ------------------------------------------------------------------------------ | | `account` | The account whose token or token_env to resolve. **TYPE:** `OAuthTokenAccount` | | RETURNS | DESCRIPTION | | ------- | ------------------------------------ | | `str` | The bearer token (no Bearer prefix). | | RAISES | DESCRIPTION | | ------------ | ------------------------------------------------------ | | `OAuthError` | If token_env is set but the env var is unset or empty. | Source code in `src/mixpanel_headless/_internal/auth/token_resolver.py` ``` def get_static_token(self, account: OAuthTokenAccount) -> str: """Return the static bearer for an :class:`OAuthTokenAccount`. Resolves the bearer from the inline ``token`` field if present; otherwise reads the environment variable named in ``token_env``. Args: account: The account whose ``token`` or ``token_env`` to resolve. Returns: The bearer token (no ``Bearer`` prefix). Raises: OAuthError: If ``token_env`` is set but the env var is unset or empty. """ if account.token is not None: return account.token.get_secret_value() env_name = account.token_env # The ``OAuthTokenAccount`` validator enforces ``token XOR token_env``, # so this branch is reachable only when ``token_env`` is set. We raise # explicitly (rather than ``assert env_name is not None``) so the # invariant survives ``python -O``, where assertions are stripped. if env_name is None: # pragma: no cover β€” model invariant raise OAuthError( f"OAuth account '{account.name}' has neither `token` nor `token_env`.", code="OAUTH_TOKEN_ERROR", details={"account_name": account.name}, ) value = os.environ.get(env_name) if not value: raise OAuthError( ( f"OAuth account '{account.name}' references env var " f"`{env_name}`, but it is not set or is empty." ), code="OAUTH_TOKEN_ERROR", details={"account_name": account.name, "env_var": env_name}, ) return value ``` Copy markdown # Exceptions All library exceptions inherit from `MixpanelHeadlessError`, enabling callers to catch all library errors with a single except clause. Explore on DeepWiki πŸ€– **[Error Handling Guide β†’](https://deepwiki.com/mixpanel/mixpanel-headless/7.4-error-codes-and-exceptions)** Ask questions about specific exceptions, error recovery patterns, or debugging strategies. ## Exception Hierarchy ``` MixpanelHeadlessError β”œβ”€β”€ ConfigError β”‚ β”œβ”€β”€ AccountNotFoundError β”‚ β”œβ”€β”€ AccountExistsError β”‚ β”œβ”€β”€ AccountInUseError β”‚ β”œβ”€β”€ InvalidArgumentError β”‚ └── ProjectNotFoundError β”œβ”€β”€ APIError β”‚ β”œβ”€β”€ AuthenticationError β”‚ β”œβ”€β”€ RateLimitError β”‚ β”œβ”€β”€ QueryError β”‚ β”œβ”€β”€ ServerError β”‚ └── JQLSyntaxError β”œβ”€β”€ OAuthError β”‚ └── RegionProbeError β”‚ └── RegionProbeNetworkError β”œβ”€β”€ WorkspaceScopeError └── BusinessContextValidationError ``` ## Catching Errors ``` import mixpanel_headless as mp try: ws = mp.Workspace() result = ws.segmentation(event="Purchase", from_date="2025-01-01", to_date="2025-01-31") except mp.AuthenticationError as e: print(f"Auth failed: {e.message}") except mp.RateLimitError as e: print(f"Rate limited, retry after {e.retry_after}s") except mp.OAuthError as e: print(f"OAuth error [{e.code}]: {e.message}") except mp.WorkspaceScopeError as e: print(f"Workspace error [{e.code}]: {e.message}") except mp.AccountInUseError as e: print(f"Account '{e.account_name}' referenced by targets: {e.referenced_by}") except mp.MixpanelHeadlessError as e: print(f"Error [{e.code}]: {e.message}") ``` ## Base Exception ## mixpanel_headless.MixpanelHeadlessError ``` MixpanelHeadlessError( message: str, code: str = "UNKNOWN_ERROR", details: dict[str, Any] | None = None, ) ``` Bases: `Exception` Base exception for all mixpanel_headless errors. All library exceptions inherit from this class, allowing callers to: - Catch all library errors: except MixpanelHeadlessError - Handle specific errors: except AccountNotFoundError - Serialize errors: error.to_dict() Initialize exception. | PARAMETER | DESCRIPTION | | --------- | ----------------------------------------------------------------------------------------------------- | | `message` | Human-readable error message. **TYPE:** `str` | | `code` | Machine-readable error code for programmatic handling. **TYPE:** `str` **DEFAULT:** `'UNKNOWN_ERROR'` | | `details` | Additional structured data about the error. **TYPE:** \`dict[str, Any] | Source code in `src/mixpanel_headless/exceptions.py` ``` def __init__( self, message: str, code: str = "UNKNOWN_ERROR", details: dict[str, Any] | None = None, ) -> None: """Initialize exception. Args: message: Human-readable error message. code: Machine-readable error code for programmatic handling. details: Additional structured data about the error. """ super().__init__(message) self._message = message self._code = code self._details = details or {} ``` ### code ``` code: str ``` Machine-readable error code. ### message ``` message: str ``` Human-readable error message. ### details ``` details: dict[str, Any] ``` Additional structured error data. ### to_dict ``` to_dict() -> dict[str, Any] ``` Serialize exception for logging/JSON output. | RETURNS | DESCRIPTION | | ---------------- | --------------------------------------------- | | `dict[str, Any]` | Dictionary with keys: code, message, details. | | `dict[str, Any]` | All values are JSON-serializable. | Source code in `src/mixpanel_headless/exceptions.py` ``` def to_dict(self) -> dict[str, Any]: """Serialize exception for logging/JSON output. Returns: Dictionary with keys: code, message, details. All values are JSON-serializable. """ return { "code": self._code, "message": self._message, "details": self._details, } ``` ### __str__ ``` __str__() -> str ``` Return human-readable error message. Source code in `src/mixpanel_headless/exceptions.py` ``` def __str__(self) -> str: """Return human-readable error message.""" return self._message ``` ### __repr__ ``` __repr__() -> str ``` Return detailed string representation. Source code in `src/mixpanel_headless/exceptions.py` ``` def __repr__(self) -> str: """Return detailed string representation.""" return ( f"{self.__class__.__name__}(message={self._message!r}, code={self._code!r})" ) ``` ## API Exceptions ## mixpanel_headless.APIError ``` APIError( message: str, *, status_code: int, response_body: str | dict[str, Any] | None = None, request_method: str | None = None, request_url: str | None = None, request_params: dict[str, Any] | None = None, request_body: dict[str, Any] | None = None, code: str = "API_ERROR", ) ``` Bases: `MixpanelHeadlessError` Base class for Mixpanel API HTTP errors. Provides structured access to HTTP request/response context for debugging and automated recovery by AI agents. All API-related exceptions inherit from this class, enabling agents to: - Understand what went wrong (status code, error message) - See exactly what was sent (request method, URL, params, body) - See exactly what came back (response body, headers) - Modify their approach and retry autonomously Example ``` try: result = client.segmentation(event="signup", ...) except APIError as e: print(f"Status: {e.status_code}") print(f"Response: {e.response_body}") print(f"Request URL: {e.request_url}") print(f"Request params: {e.request_params}") ``` Initialize APIError. | PARAMETER | DESCRIPTION | | ---------------- | ----------------------------------------------------------------------- | | `message` | Human-readable error message. **TYPE:** `str` | | `status_code` | HTTP status code from response. **TYPE:** `int` | | `response_body` | Raw response body (string or parsed dict). **TYPE:** \`str | | `request_method` | HTTP method used (GET, POST). **TYPE:** \`str | | `request_url` | Full request URL. **TYPE:** \`str | | `request_params` | Query parameters sent. **TYPE:** \`dict[str, Any] | | `request_body` | Request body sent (for POST requests). **TYPE:** \`dict[str, Any] | | `code` | Machine-readable error code. **TYPE:** `str` **DEFAULT:** `'API_ERROR'` | Source code in `src/mixpanel_headless/exceptions.py` ``` def __init__( self, message: str, *, status_code: int, response_body: str | dict[str, Any] | None = None, request_method: str | None = None, request_url: str | None = None, request_params: dict[str, Any] | None = None, request_body: dict[str, Any] | None = None, code: str = "API_ERROR", ) -> None: """Initialize APIError. Args: message: Human-readable error message. status_code: HTTP status code from response. response_body: Raw response body (string or parsed dict). request_method: HTTP method used (GET, POST). request_url: Full request URL. request_params: Query parameters sent. request_body: Request body sent (for POST requests). code: Machine-readable error code. """ self._status_code = status_code self._response_body = response_body self._request_method = request_method self._request_url = request_url self._request_params = request_params self._request_body = request_body details: dict[str, Any] = { "status_code": status_code, } if response_body is not None: details["response_body"] = response_body if request_method is not None: details["request_method"] = request_method if request_url is not None: details["request_url"] = request_url if request_params is not None: details["request_params"] = request_params if request_body is not None: details["request_body"] = request_body super().__init__(message, code=code, details=details) ``` ### status_code ``` status_code: int ``` HTTP status code from response. ### response_body ``` response_body: str | dict[str, Any] | None ``` Raw response body (string or parsed dict). ### request_method ``` request_method: str | None ``` HTTP method used (GET, POST). ### request_url ``` request_url: str | None ``` Full request URL. ### request_params ``` request_params: dict[str, Any] | None ``` Query parameters sent. ### request_body ``` request_body: dict[str, Any] | None ``` Request body sent (for POST requests). ## mixpanel_headless.AuthenticationError ``` AuthenticationError( message: str = "Authentication failed", *, status_code: int = 401, response_body: str | dict[str, Any] | None = None, request_method: str | None = None, request_url: str | None = None, request_params: dict[str, Any] | None = None, ) ``` Bases: `APIError` Authentication with Mixpanel API failed (HTTP 401). Raised when credentials are invalid, expired, or lack required permissions. Inherits from APIError to provide full request/response context. Example ``` try: client.segmentation(...) except AuthenticationError as e: print(f"Auth failed: {e.message}") print(f"Request URL: {e.request_url}") # Check if project_id is correct, credentials are valid, etc. ``` Initialize AuthenticationError. | PARAMETER | DESCRIPTION | | ---------------- | ------------------------------------------------------------------------------------ | | `message` | Human-readable error message. **TYPE:** `str` **DEFAULT:** `'Authentication failed'` | | `status_code` | HTTP status code (default 401). **TYPE:** `int` **DEFAULT:** `401` | | `response_body` | Raw response body. **TYPE:** \`str | | `request_method` | HTTP method used. **TYPE:** \`str | | `request_url` | Full request URL. **TYPE:** \`str | | `request_params` | Query parameters sent. **TYPE:** \`dict[str, Any] | Source code in `src/mixpanel_headless/exceptions.py` ``` def __init__( self, message: str = "Authentication failed", *, status_code: int = 401, response_body: str | dict[str, Any] | None = None, request_method: str | None = None, request_url: str | None = None, request_params: dict[str, Any] | None = None, ) -> None: """Initialize AuthenticationError. Args: message: Human-readable error message. status_code: HTTP status code (default 401). response_body: Raw response body. request_method: HTTP method used. request_url: Full request URL. request_params: Query parameters sent. """ super().__init__( message, status_code=status_code, response_body=response_body, request_method=request_method, request_url=request_url, request_params=request_params, code="AUTH_FAILED", ) ``` ## mixpanel_headless.RateLimitError ``` RateLimitError( message: str = "Rate limit exceeded", *, retry_after: int | None = None, status_code: int = 429, response_body: str | dict[str, Any] | None = None, request_method: str | None = None, request_url: str | None = None, request_params: dict[str, Any] | None = None, ) ``` Bases: `APIError` Mixpanel API rate limit exceeded (HTTP 429). Raised when the API returns a 429 status. The retry_after property indicates when the request can be retried. Inherits from APIError to provide full request context for debugging. Example ``` try: for _ in range(1000): client.segmentation(...) except RateLimitError as e: print(f"Rate limited! Retry after {e.retry_after}s") print(f"Request: {e.request_method} {e.request_url}") time.sleep(e.retry_after or 60) ``` Initialize RateLimitError. | PARAMETER | DESCRIPTION | | ---------------- | ---------------------------------------------------------------------------------- | | `message` | Human-readable error message. **TYPE:** `str` **DEFAULT:** `'Rate limit exceeded'` | | `retry_after` | Seconds until retry is allowed (from Retry-After header). **TYPE:** \`int | | `status_code` | HTTP status code (default 429). **TYPE:** `int` **DEFAULT:** `429` | | `response_body` | Raw response body. **TYPE:** \`str | | `request_method` | HTTP method used. **TYPE:** \`str | | `request_url` | Full request URL. **TYPE:** \`str | | `request_params` | Query parameters sent. **TYPE:** \`dict[str, Any] | Source code in `src/mixpanel_headless/exceptions.py` ``` def __init__( self, message: str = "Rate limit exceeded", *, retry_after: int | None = None, status_code: int = 429, response_body: str | dict[str, Any] | None = None, request_method: str | None = None, request_url: str | None = None, request_params: dict[str, Any] | None = None, ) -> None: """Initialize RateLimitError. Args: message: Human-readable error message. retry_after: Seconds until retry is allowed (from Retry-After header). status_code: HTTP status code (default 429). response_body: Raw response body. request_method: HTTP method used. request_url: Full request URL. request_params: Query parameters sent. """ self._retry_after = retry_after if retry_after is not None: message = f"{message}. Retry after {retry_after} seconds." super().__init__( message, status_code=status_code, response_body=response_body, request_method=request_method, request_url=request_url, request_params=request_params, code="RATE_LIMITED", ) # Add retry_after to details if retry_after is not None: self._details["retry_after"] = retry_after ``` ### retry_after ``` retry_after: int | None ``` Seconds until retry is allowed, or None if unknown. ## mixpanel_headless.QueryError ``` QueryError( message: str = "Query execution failed", *, status_code: int = 400, response_body: str | dict[str, Any] | None = None, request_method: str | None = None, request_url: str | None = None, request_params: dict[str, Any] | None = None, request_body: dict[str, Any] | None = None, ) ``` Bases: `APIError` Query execution failed (HTTP 400 or query-specific error). Raised when an API query fails due to invalid parameters, syntax errors, or other query-specific issues. Inherits from APIError to provide full request/response context for debugging. Example ``` try: client.segmentation(event="nonexistent", ...) except QueryError as e: print(f"Query failed: {e.message}") print(f"Response: {e.response_body}") print(f"Request params: {e.request_params}") ``` Initialize QueryError. | PARAMETER | DESCRIPTION | | ---------------- | ------------------------------------------------------------------------------------- | | `message` | Human-readable error message. **TYPE:** `str` **DEFAULT:** `'Query execution failed'` | | `status_code` | HTTP status code (default 400). **TYPE:** `int` **DEFAULT:** `400` | | `response_body` | Raw response body with error details. **TYPE:** \`str | | `request_method` | HTTP method used. **TYPE:** \`str | | `request_url` | Full request URL. **TYPE:** \`str | | `request_params` | Query parameters sent. **TYPE:** \`dict[str, Any] | | `request_body` | Request body sent (for POST). **TYPE:** \`dict[str, Any] | Source code in `src/mixpanel_headless/exceptions.py` ``` def __init__( self, message: str = "Query execution failed", *, status_code: int = 400, response_body: str | dict[str, Any] | None = None, request_method: str | None = None, request_url: str | None = None, request_params: dict[str, Any] | None = None, request_body: dict[str, Any] | None = None, ) -> None: """Initialize QueryError. Args: message: Human-readable error message. status_code: HTTP status code (default 400). response_body: Raw response body with error details. request_method: HTTP method used. request_url: Full request URL. request_params: Query parameters sent. request_body: Request body sent (for POST). """ super().__init__( message, status_code=status_code, response_body=response_body, request_method=request_method, request_url=request_url, request_params=request_params, request_body=request_body, code="QUERY_FAILED", ) ``` ## mixpanel_headless.ServerError ``` ServerError( message: str = "Server error", *, status_code: int = 500, response_body: str | dict[str, Any] | None = None, request_method: str | None = None, request_url: str | None = None, request_params: dict[str, Any] | None = None, request_body: dict[str, Any] | None = None, ) ``` Bases: `APIError` Mixpanel server error (HTTP 5xx). Raised when the Mixpanel API returns a server error. These are typically transient issues that may succeed on retry. The response_body property contains the full error details from Mixpanel, which often include actionable information (e.g., "unit and interval both specified"). Example ``` try: client.retention(born_event="signup", ...) except ServerError as e: print(f"Server error {e.status_code}: {e.message}") print(f"Response: {e.response_body}") print(f"Request params: {e.request_params}") # AI agent can analyze response_body to fix the request ``` Initialize ServerError. | PARAMETER | DESCRIPTION | | ---------------- | --------------------------------------------------------------------------- | | `message` | Human-readable error message. **TYPE:** `str` **DEFAULT:** `'Server error'` | | `status_code` | HTTP status code (5xx). **TYPE:** `int` **DEFAULT:** `500` | | `response_body` | Raw response body with error details. **TYPE:** \`str | | `request_method` | HTTP method used. **TYPE:** \`str | | `request_url` | Full request URL. **TYPE:** \`str | | `request_params` | Query parameters sent. **TYPE:** \`dict[str, Any] | | `request_body` | Request body sent (for POST). **TYPE:** \`dict[str, Any] | Source code in `src/mixpanel_headless/exceptions.py` ``` def __init__( self, message: str = "Server error", *, status_code: int = 500, response_body: str | dict[str, Any] | None = None, request_method: str | None = None, request_url: str | None = None, request_params: dict[str, Any] | None = None, request_body: dict[str, Any] | None = None, ) -> None: """Initialize ServerError. Args: message: Human-readable error message. status_code: HTTP status code (5xx). response_body: Raw response body with error details. request_method: HTTP method used. request_url: Full request URL. request_params: Query parameters sent. request_body: Request body sent (for POST). """ super().__init__( message, status_code=status_code, response_body=response_body, request_method=request_method, request_url=request_url, request_params=request_params, request_body=request_body, code="SERVER_ERROR", ) ``` ## mixpanel_headless.JQLSyntaxError ``` JQLSyntaxError( raw_error: str, script: str | None = None, request_path: str | None = None ) ``` Bases: `QueryError` JQL script execution failed with syntax or runtime error (HTTP 412). Raised when a JQL script fails to execute due to syntax errors, type errors, or other JavaScript runtime issues. Provides structured access to error details from Mixpanel's response. Inherits from QueryError (and thus APIError) to provide full HTTP context. Example ``` try: result = live_query.jql(script) except JQLSyntaxError as e: print(f"Error: {e.error_type}: {e.error_message}") print(f"Script: {e.script}") print(f"Line info: {e.line_info}") # AI agent can use this to fix the script and retry ``` Initialize JQLSyntaxError. | PARAMETER | DESCRIPTION | | -------------- | ------------------------------------------------------------ | | `raw_error` | Raw error string from Mixpanel API response. **TYPE:** `str` | | `script` | The JQL script that caused the error. **TYPE:** \`str | | `request_path` | API request path from error response. **TYPE:** \`str | Source code in `src/mixpanel_headless/exceptions.py` ``` def __init__( self, raw_error: str, script: str | None = None, request_path: str | None = None, ) -> None: """Initialize JQLSyntaxError. Args: raw_error: Raw error string from Mixpanel API response. script: The JQL script that caused the error. request_path: API request path from error response. """ # Parse structured error info from raw error string self._error_type = self._extract_error_type(raw_error) self._error_message = self._extract_message(raw_error) self._line_info = self._extract_line_info(raw_error) self._stack_trace = self._extract_stack_trace(raw_error) self._script = script self._raw_error = raw_error self._request_path = request_path # Build human-readable message message = f"JQL {self._error_type}: {self._error_message}" if self._line_info: message += f"\n{self._line_info}" # Build response body dict for APIError response_body: dict[str, Any] = { "error": raw_error, } if request_path: response_body["request"] = request_path super().__init__( message, status_code=412, response_body=response_body, request_body={"script": script} if script else None, ) self._code = "JQL_SYNTAX_ERROR" # Add JQL-specific details self._details["error_type"] = self._error_type self._details["error_message"] = self._error_message self._details["line_info"] = self._line_info self._details["stack_trace"] = self._stack_trace self._details["script"] = script self._details["request_path"] = request_path self._details["raw_error"] = raw_error ``` ### error_type ``` error_type: str ``` JavaScript error type (TypeError, SyntaxError, ReferenceError, etc.). ### error_message ``` error_message: str ``` Error message describing what went wrong. ### line_info ``` line_info: str | None ``` Code snippet with caret showing error location, if available. ### stack_trace ``` stack_trace: str | None ``` JavaScript stack trace, if available. ### script ``` script: str | None ``` The JQL script that caused the error. ### raw_error ``` raw_error: str ``` Complete raw error string from Mixpanel. ## Configuration Exceptions ## mixpanel_headless.ConfigError ``` ConfigError(message: str, details: dict[str, Any] | None = None) ``` Bases: `MixpanelHeadlessError` Base for configuration-related errors. Raised when there's a problem with configuration files, environment variables, or credential resolution. Initialize ConfigError. | PARAMETER | DESCRIPTION | | --------- | ------------------------------------------------------ | | `message` | Human-readable error message. **TYPE:** `str` | | `details` | Additional structured data. **TYPE:** \`dict[str, Any] | Source code in `src/mixpanel_headless/exceptions.py` ``` def __init__( self, message: str, details: dict[str, Any] | None = None, ) -> None: """Initialize ConfigError. Args: message: Human-readable error message. details: Additional structured data. """ super().__init__(message, code="CONFIG_ERROR", details=details) ``` ## mixpanel_headless.AccountNotFoundError ``` AccountNotFoundError( account_name: str, available_accounts: list[str] | None = None ) ``` Bases: `ConfigError` Named account does not exist in configuration. Raised when attempting to access an account that hasn't been configured. The available_accounts property lists valid account names to help users. Initialize AccountNotFoundError. | PARAMETER | DESCRIPTION | | -------------------- | ------------------------------------------------------------------ | | `account_name` | The requested account name that wasn't found. **TYPE:** `str` | | `available_accounts` | List of valid account names for suggestions. **TYPE:** \`list[str] | Source code in `src/mixpanel_headless/exceptions.py` ``` def __init__( self, account_name: str, available_accounts: list[str] | None = None, ) -> None: """Initialize AccountNotFoundError. Args: account_name: The requested account name that wasn't found. available_accounts: List of valid account names for suggestions. """ available = available_accounts or [] if available: available_str = ", ".join(f"'{a}'" for a in available) message = ( f"Account '{account_name}' not found. " f"Available accounts: {available_str}" ) else: message = f"Account '{account_name}' not found. No accounts configured." details = { "account_name": account_name, "available_accounts": available, } super().__init__(message, details=details) self._code = "ACCOUNT_NOT_FOUND" ``` ### account_name ``` account_name: str ``` The requested account name that wasn't found. ### available_accounts ``` available_accounts: list[str] ``` List of valid account names. ## mixpanel_headless.AccountExistsError ``` AccountExistsError(account_name: str) ``` Bases: `ConfigError` Account name already exists in configuration. Raised when attempting to add an account with a name that's already in use. Initialize AccountExistsError. | PARAMETER | DESCRIPTION | | -------------- | --------------------------------------------- | | `account_name` | The conflicting account name. **TYPE:** `str` | Source code in `src/mixpanel_headless/exceptions.py` ``` def __init__(self, account_name: str) -> None: """Initialize AccountExistsError. Args: account_name: The conflicting account name. """ message = f"Account '{account_name}' already exists." details = {"account_name": account_name} super().__init__(message, details=details) self._code = "ACCOUNT_EXISTS" ``` ### account_name ``` account_name: str ``` The conflicting account name. ## mixpanel_headless.AccountInUseError ``` AccountInUseError(account_name: str, referenced_by: list[str] | None = None) ``` Bases: `ConfigError` Account is referenced by one or more targets and cannot be removed. Raised by `mp.accounts.remove(name)` when the account is referenced by one or more `[targets.NAME]` blocks and the caller did not pass `force=True`. The list of dependent target names is available in `referenced_by` so callers can show a helpful error message or pass `force=True` to delete the account and orphan the targets. Initialize AccountInUseError. | PARAMETER | DESCRIPTION | | --------------- | ------------------------------------------------------------------ | | `account_name` | The account that callers tried to remove. **TYPE:** `str` | | `referenced_by` | Names of targets that reference the account. **TYPE:** \`list[str] | Source code in `src/mixpanel_headless/exceptions.py` ``` def __init__( self, account_name: str, referenced_by: list[str] | None = None ) -> None: """Initialize AccountInUseError. Args: account_name: The account that callers tried to remove. referenced_by: Names of targets that reference the account. """ targets = referenced_by or [] if targets: target_str = ", ".join(f"'{t}'" for t in targets) message = ( f"Account '{account_name}' is referenced by target(s): {target_str}. " f"Pass `force=True` to remove anyway." ) else: message = ( f"Account '{account_name}' is in use. Pass `force=True` to remove." ) details: dict[str, Any] = { "account_name": account_name, "referenced_by": list(targets), } super().__init__(message, details=details) self._code = "ACCOUNT_IN_USE" ``` ### account_name ``` account_name: str ``` The account name that callers tried to remove. ### referenced_by ``` referenced_by: list[str] ``` Target names that reference the account. ## mixpanel_headless.ProjectNotFoundError ``` ProjectNotFoundError( project_id: str, available_projects: list[str] | None = None ) ``` Bases: `ConfigError` Raised when a specified project is not accessible. Includes the requested project ID and optionally a list of accessible project IDs to help the user correct their selection. Example ``` try: projects = ws.projects() match = [p for p in projects if p.id == target_id] if not match: raise ProjectNotFoundError( target_id, available_projects=[p.id for p in projects], ) except ProjectNotFoundError as e: print(f"Project '{e.project_id}' not found.") if e.available_projects: print(f"Available: {', '.join(e.available_projects)}") ``` Initialize ProjectNotFoundError. | PARAMETER | DESCRIPTION | | -------------------- | --------------------------------------------------------------------- | | `project_id` | The requested project ID that wasn't found. **TYPE:** `str` | | `available_projects` | List of accessible project IDs for suggestions. **TYPE:** \`list[str] | Source code in `src/mixpanel_headless/exceptions.py` ``` def __init__( self, project_id: str, available_projects: list[str] | None = None, ) -> None: """Initialize ProjectNotFoundError. Args: project_id: The requested project ID that wasn't found. available_projects: List of accessible project IDs for suggestions. """ available = available_projects or [] if available: available_str = ", ".join(f"'{p}'" for p in available) message = ( f"Project '{project_id}' not found. Available projects: {available_str}" ) else: message = ( f"Project '{project_id}' not found. No accessible projects discovered." ) details: dict[str, Any] = { "project_id": project_id, "available_projects": available, } super().__init__(message, details=details) self._code = "PROJECT_NOT_FOUND" ``` ### project_id ``` project_id: str ``` The requested project ID that wasn't found. ### available_projects ``` available_projects: list[str] ``` List of accessible project IDs. ### InvalidArgumentError Raised by `accounts.login_unified` (and the CLI's `mp login`) when a public-API call combines mutually incompatible arguments. Subclass of `ConfigError`. The CLI maps this to exit code 3 (`INVALID_ARGS`) instead of the generic 1. | `violation` | Raised When | | --------------------- | ---------------------------------------------------------- | | `mutually_exclusive` | `--service-account` + `--token-env` (or equivalent kwargs) | | `no_browser_misuse` | `--no-browser` against a non-browser auth type | | `secret_stdin_misuse` | `--secret-stdin` against a non-SA auth type | The `details` dict carries `violation` and (when detection ran) `detected_auth_type`. Pattern-match by class so non-CLI callers (Cowork's `auth_manager.py`, JSON consumers) can dispatch without parsing the human message. ## mixpanel_headless.InvalidArgumentError ``` InvalidArgumentError( message: str, *, violation: Literal[ "mutually_exclusive", "no_browser_misuse", "secret_stdin_misuse" ], detected_auth_type: str | None = None, ) ``` Bases: `ConfigError` Raised when a public API call combines mutually incompatible arguments. Carries a `violation` discriminator and the resolved `detected_auth_type` so non-CLI callers (Cowork's `auth_manager.py`, JSON consumers) can dispatch programmatically without parsing the human message. The CLI `handle_errors` decorator maps this subclass to `ExitCode.INVALID_ARGS` (3) instead of the generic `GENERAL_ERROR` (1) that `ConfigError` would otherwise produce. Used by `accounts.login_unified` for the three documented flag-combination rejections (043 contract, `cli-commands.md` Β§5): `--service-account` + `--token-env`, `--no-browser` against a non-browser auth type, and `--secret-stdin` against a non-SA auth type. Example ``` try: accounts.login_unified(service_account=True, token_env="X") except InvalidArgumentError as exc: assert exc.violation == "mutually_exclusive" assert exc.detected_auth_type == "service_account" ``` Initialize InvalidArgumentError. | PARAMETER | DESCRIPTION | | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `message` | Human-readable error message. **TYPE:** `str` | | `violation` | Discriminator for the kind of misuse. One of "mutually_exclusive", "no_browser_misuse", "secret_stdin_misuse". **TYPE:** `Literal['mutually_exclusive', 'no_browser_misuse', 'secret_stdin_misuse']` | | `detected_auth_type` | The auth type the orchestrator resolved from the supplied flags / env. None only when the violation was caught BEFORE detection ran (currently no such case, but kept optional for future-proofing). **TYPE:** \`str | Source code in `src/mixpanel_headless/exceptions.py` ``` def __init__( self, message: str, *, violation: Literal[ "mutually_exclusive", "no_browser_misuse", "secret_stdin_misuse" ], detected_auth_type: str | None = None, ) -> None: """Initialize InvalidArgumentError. Args: message: Human-readable error message. violation: Discriminator for the kind of misuse. One of ``"mutually_exclusive"``, ``"no_browser_misuse"``, ``"secret_stdin_misuse"``. detected_auth_type: The auth type the orchestrator resolved from the supplied flags / env. ``None`` only when the violation was caught BEFORE detection ran (currently no such case, but kept optional for future-proofing). """ if violation not in self._VALID_VIOLATIONS: raise ValueError( f"Invalid violation {violation!r}; must be one of " f"{self._VALID_VIOLATIONS}." ) details: dict[str, Any] = {"violation": violation} if detected_auth_type is not None: details["detected_auth_type"] = detected_auth_type super().__init__(message, details=details) self._code = "INVALID_ARGUMENT" ``` ### violation ``` violation: str ``` The kind of misuse β€” see `_VALID_VIOLATIONS`. ### detected_auth_type ``` detected_auth_type: str | None ``` The auth type the orchestrator resolved (or `None` if pre-detection). ## OAuth Exceptions Raised during OAuth 2.0 PKCE authentication flows and the `mp login` region probe. | Error Code | Raised When | | --------------------------- | ------------------------------------------------------------------------------------------------------------------ | | `OAUTH_TOKEN_ERROR` | Token exchange fails | | `OAUTH_REFRESH_ERROR` | Token refresh fails (transient) | | `OAUTH_REFRESH_REVOKED` | Refresh token rejected by IdP as `invalid_grant` (re-run `mp login --name NAME`) | | `OAUTH_REGISTRATION_ERROR` | Dynamic client registration fails | | `OAUTH_TIMEOUT` | Callback server times out waiting for authorization | | `OAUTH_PORT_ERROR` | Cannot bind to a local port for the callback server | | `OAUTH_BROWSER_ERROR` | Cannot open the authorization URL in the browser | | `OAUTH_REGION_PROBE_FAILED` | `mp login` probed every region and none accepted the credential β€” see `RegionProbeError` below | | `OAUTH_NETWORK_UNREACHABLE` | Every region probe failed at the network layer (DNS / TLS / connect refused) β€” see `RegionProbeNetworkError` below | ## mixpanel_headless.OAuthError ``` OAuthError( message: str, code: str = "OAUTH_TOKEN_ERROR", details: dict[str, Any] | None = None, ) ``` Bases: `MixpanelHeadlessError` OAuth authentication flow error. Raised for failures during the OAuth 2.0 PKCE flow, including token exchange, token refresh, client registration, callback timeout, port unavailability, and browser launch failures. Error codes: - OAUTH_TOKEN_ERROR: Token exchange or validation failed - OAUTH_REFRESH_ERROR: Token refresh failed - OAUTH_REGISTRATION_ERROR: Dynamic Client Registration failed - OAUTH_TIMEOUT: Callback server timed out waiting for authorization - OAUTH_PORT_ERROR: All callback ports are occupied - OAUTH_BROWSER_ERROR: Could not open browser for authorization Example ``` try: flow = OAuthFlow(region="us") tokens = flow.login() except OAuthError as e: print(f"OAuth failed: {e.message} (code: {e.code})") ``` Initialize OAuthError. | PARAMETER | DESCRIPTION | | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `message` | Human-readable error message. **TYPE:** `str` | | `code` | Machine-readable error code. One of: OAUTH_TOKEN_ERROR, OAUTH_REFRESH_ERROR, OAUTH_REGISTRATION_ERROR, OAUTH_TIMEOUT, OAUTH_PORT_ERROR, OAUTH_BROWSER_ERROR. **TYPE:** `str` **DEFAULT:** `'OAUTH_TOKEN_ERROR'` | | `details` | Additional structured data about the error. **TYPE:** \`dict[str, Any] | Source code in `src/mixpanel_headless/exceptions.py` ``` def __init__( self, message: str, code: str = "OAUTH_TOKEN_ERROR", details: dict[str, Any] | None = None, ) -> None: """Initialize OAuthError. Args: message: Human-readable error message. code: Machine-readable error code. One of: OAUTH_TOKEN_ERROR, OAUTH_REFRESH_ERROR, OAUTH_REGISTRATION_ERROR, OAUTH_TIMEOUT, OAUTH_PORT_ERROR, OAUTH_BROWSER_ERROR. details: Additional structured data about the error. """ super().__init__(message, code=code, details=details) ``` ### RegionProbeError Raised by `mp login` (and `accounts.login_unified`) when the `us β†’ eu β†’ in` region probe fails for every region. Subclass of `OAuthError`. The `attempts` attribute carries the full `(region, status_code, error_body)` list; status `0` indicates a network-layer failure (DNS / TLS / connect refused) β€” those cases raise `RegionProbeNetworkError` (subclass) so the CLI can render a different remediation hint. ``` import mixpanel_headless as mp try: mp.accounts.login_unified() except mp.RegionProbeNetworkError as exc: print("Could not reach any Mixpanel region. Check connectivity.") for region, status, body in exc.attempts: print(f" {region}: {body}") except mp.RegionProbeError as exc: print("Credential not valid in any region.") for region, status, body in exc.attempts: print(f" {region}: {status} {body}") ``` ## mixpanel_headless.RegionProbeError ``` RegionProbeError( message: str, *, attempts: list[tuple[Region, int, str]], code: str = "OAUTH_REGION_PROBE_FAILED", ) ``` Bases: `OAuthError` Raised when no region accepts the credential during region probing. The region probe walks a configured order (default `us` β†’ `eu` β†’ `in`) against `/api/app/me`, returning the first 200. When every probe attempt fails, this exception is raised carrying the full attempt list for diagnostic and telemetry use. A status code of `0` indicates the request never reached the server (network error); the third tuple element carries the failure detail (HTTP response text or the network error reason). See :class:`RegionProbeNetworkError` for the all-network-error subclass β€” the probe distinguishes "credential rejected" from "could not reach any region" so the CLI can render different remediation hints. Example ``` try: result = probe_region(client_factory, headers) except RegionProbeError as exc: for region, status, body in exc.attempts: print(f"{region}: {status} {body}") ``` Initialize RegionProbeError. | PARAMETER | DESCRIPTION | | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `message` | Human-readable error message. **TYPE:** `str` | | `attempts` | Ordered list of (region, status_code, error_body) tuples for every probed region. status_code is 0 for network errors; error_body carries the failure detail. **TYPE:** `list[tuple[Region, int, str]]` | | `code` | Machine-readable error code. Defaults to OAUTH_REGION_PROBE_FAILED for the generic case; :class:RegionProbeNetworkError overrides to OAUTH_NETWORK_UNREACHABLE. **TYPE:** `str` **DEFAULT:** `'OAUTH_REGION_PROBE_FAILED'` | Source code in `src/mixpanel_headless/exceptions.py` ``` def __init__( self, message: str, *, attempts: list[tuple[Region, int, str]], code: str = "OAUTH_REGION_PROBE_FAILED", ) -> None: """Initialize RegionProbeError. Args: message: Human-readable error message. attempts: Ordered list of ``(region, status_code, error_body)`` tuples for every probed region. ``status_code`` is ``0`` for network errors; ``error_body`` carries the failure detail. code: Machine-readable error code. Defaults to ``OAUTH_REGION_PROBE_FAILED`` for the generic case; :class:`RegionProbeNetworkError` overrides to ``OAUTH_NETWORK_UNREACHABLE``. """ self._attempts: list[tuple[Region, int, str]] = list(attempts) super().__init__( message, code=code, details={"attempts": [list(a) for a in self._attempts]}, ) ``` ### attempts ``` attempts: list[tuple[Region, int, str]] ``` Ordered list of `(region, status_code, error_body)` tuples. ### to_dict ``` to_dict() -> dict[str, Any] ``` Serialize the exception to a JSON-friendly dict. Includes `attempts` at the top level so consumers can inspect the per-region probe outcomes without unpacking `details`. | RETURNS | DESCRIPTION | | ---------------- | ------------------------------------------------- | | `dict[str, Any]` | Dictionary with keys code, message, details, and | | `dict[str, Any]` | attempts. Each attempts entry is a 3-element list | | `dict[str, Any]` | [region, status_code, error_body]. | Source code in `src/mixpanel_headless/exceptions.py` ``` def to_dict(self) -> dict[str, Any]: """Serialize the exception to a JSON-friendly dict. Includes ``attempts`` at the top level so consumers can inspect the per-region probe outcomes without unpacking ``details``. Returns: Dictionary with keys ``code``, ``message``, ``details``, and ``attempts``. Each ``attempts`` entry is a 3-element list ``[region, status_code, error_body]``. """ base = super().to_dict() base["attempts"] = [list(a) for a in self._attempts] return base ``` ## mixpanel_headless.RegionProbeNetworkError ``` RegionProbeNetworkError( message: str, *, attempts: list[tuple[Region, int, str]] ) ``` Bases: `RegionProbeError` Raised when every region probe attempt failed at the network layer. Subclass of :class:`RegionProbeError` used when ALL recorded attempts have `status_code == 0` β€” i.e. the credential was never actually evaluated because no region was reachable (DNS failure, TLS rejection, captive portal, no internet). The CLI catches this before the generic `RegionProbeError` so it can render "could not reach any Mixpanel region" instead of "credential not valid", which would mislead a user who is actually offline. Carries the same `attempts` shape as the parent so existing consumers can render the per-region detail without changes. Initialize RegionProbeNetworkError. | PARAMETER | DESCRIPTION | | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `message` | Human-readable error message. **TYPE:** `str` | | `attempts` | Ordered list of (region, 0, error_body) tuples β€” every entry must have status 0 by construction (the probe loop only raises this subclass when that invariant holds). **TYPE:** `list[tuple[Region, int, str]]` | Source code in `src/mixpanel_headless/exceptions.py` ``` def __init__( self, message: str, *, attempts: list[tuple[Region, int, str]], ) -> None: """Initialize RegionProbeNetworkError. Args: message: Human-readable error message. attempts: Ordered list of ``(region, 0, error_body)`` tuples β€” every entry must have status 0 by construction (the probe loop only raises this subclass when that invariant holds). """ super().__init__( message, attempts=attempts, code="OAUTH_NETWORK_UNREACHABLE", ) ``` ## Workspace / Organization Scope Exceptions Raised when an auth-axis identifier (workspace or organization) cannot be resolved during App API requests. | Error Code | Raised When | | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `NO_WORKSPACES` | No workspaces found for the project | | `AMBIGUOUS_WORKSPACE` | Multiple workspaces found and none is marked as default | | `WORKSPACE_NOT_FOUND` | Specified workspace ID does not exist | | `ORGANIZATION_AMBIGUOUS` | An org-scoped business-context call could not auto-resolve the organization (active project absent from `/me` AND >1 accessible organization). `details` carries `project_id` and `available_organizations`. Pass `organization_id=N` explicitly to bypass auto-resolution. | ## mixpanel_headless.WorkspaceScopeError ``` WorkspaceScopeError( message: str, code: str = "NO_WORKSPACES", details: dict[str, Any] | None = None, ) ``` Bases: `MixpanelHeadlessError` Scope resolution error (workspace or organization). Raised when an auth-axis identifier cannot be resolved during App API requests. Originally introduced for workspace resolution; also raised when the organization ID for an org-scoped business-context call cannot be auto-derived from the cached `/me` response. Error codes: - NO_WORKSPACES: Project has no accessible workspaces - AMBIGUOUS_WORKSPACE: Multiple workspaces, none default; must specify --workspace-id - WORKSPACE_NOT_FOUND: Explicit workspace ID doesn't match any workspace - ORGANIZATION_AMBIGUOUS: Cannot auto-resolve the organization for an org-scoped call (active project absent from /me AND >1 accessible organization). The `details` dict carries `project_id` and `available_organizations`. Pass `organization_id=N` explicitly to bypass auto-resolution. Example ``` try: workspace_id = ws.resolve_workspace_id() except WorkspaceScopeError as e: print(f"Scope issue: {e.message} (code: {e.code})") ``` Initialize WorkspaceScopeError. | PARAMETER | DESCRIPTION | | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `message` | Human-readable error message. **TYPE:** `str` | | `code` | Machine-readable error code. One of: NO_WORKSPACES, AMBIGUOUS_WORKSPACE, WORKSPACE_NOT_FOUND, ORGANIZATION_AMBIGUOUS. **TYPE:** `str` **DEFAULT:** `'NO_WORKSPACES'` | | `details` | Additional structured data about the error. **TYPE:** \`dict[str, Any] | Source code in `src/mixpanel_headless/exceptions.py` ``` def __init__( self, message: str, code: str = "NO_WORKSPACES", details: dict[str, Any] | None = None, ) -> None: """Initialize WorkspaceScopeError. Args: message: Human-readable error message. code: Machine-readable error code. One of: NO_WORKSPACES, AMBIGUOUS_WORKSPACE, WORKSPACE_NOT_FOUND, ORGANIZATION_AMBIGUOUS. details: Additional structured data about the error. """ super().__init__(message, code=code, details=details) ``` ## Business Context Exceptions Raised by `Workspace.set_business_context()` when content exceeds the 50,000-character cap. The check runs **before** the HTTP call, so callers fail fast and don't waste a round-trip; the server enforces the same limit and would otherwise return `QueryError` (HTTP 400). See the [Business Context guide](https://mixpanel.github.io/mixpanel-headless/guide/business-context/index.md) for usage. | Error Code | Raised When | | --------------------------- | ---------------------------------------------------- | | `BUSINESS_CONTEXT_TOO_LONG` | `len(content) > BUSINESS_CONTEXT_MAX_CHARS` (50,000) | The `details` dict carries `length` (the actual content length) and `max` (the configured limit) for programmatic recovery. ## mixpanel_headless.BusinessContextValidationError ``` BusinessContextValidationError( message: str, details: dict[str, Any] | None = None ) ``` Bases: `MixpanelHeadlessError` Business context content failed client-side validation. Raised by `Workspace.set_business_context()` when the supplied content exceeds `BUSINESS_CONTEXT_MAX_CHARS` (50,000 characters). The check runs before the HTTP call so callers can fail fast and avoid a wasted round-trip β€” the server enforces the same limit server-side and would otherwise return `QueryError` (HTTP 400). The `details` dict carries `length` (the actual content length) and `max` (the configured limit) for programmatic recovery. Example ``` try: ws.set_business_context("x" * 60_000, level="project") except BusinessContextValidationError as e: print(f"Too long: {e.details['length']} > {e.details['max']}") ``` Initialize BusinessContextValidationError. | PARAMETER | DESCRIPTION | | --------- | --------------------------------------------------------------------------------- | | `message` | Human-readable error message. **TYPE:** `str` | | `details` | Additional structured data β€” typically length and max. **TYPE:** \`dict[str, Any] | Source code in `src/mixpanel_headless/exceptions.py` ``` def __init__( self, message: str, details: dict[str, Any] | None = None, ) -> None: """Initialize BusinessContextValidationError. Args: message: Human-readable error message. details: Additional structured data β€” typically ``length`` and ``max``. """ super().__init__( message, code="BUSINESS_CONTEXT_TOO_LONG", details=details, ) ``` Copy markdown # Result Types Explore on DeepWiki πŸ€– **[Result Types Reference β†’](https://deepwiki.com/mixpanel/mixpanel-headless/7.5-result-type-reference)** Ask questions about result structures, DataFrame conversion, or type usage patterns. All result types are immutable frozen dataclasses with: - Lazy DataFrame conversion via the `.df` property - JSON serialization via the `.to_dict()` method - Full type hints for IDE/mypy support ## App API Types Types for the Mixpanel App API infrastructure. ## mixpanel_headless.PublicWorkspace Bases: `BaseModel` A workspace within a Mixpanel project. Represents a workspace as returned by the Mixpanel App API `GET /api/app/projects/{pid}/workspaces/public` endpoint. Extra fields from the API response are preserved via `extra="allow"`. | ATTRIBUTE | DESCRIPTION | | --------------- | ------------------------------------------------------- | | `id` | Workspace identifier. **TYPE:** `int` | | `name` | Human-readable workspace name. **TYPE:** `str` | | `project_id` | Parent project identifier. **TYPE:** `int` | | `is_default` | Whether this is the default workspace. **TYPE:** `bool` | | `description` | Workspace description, if set. **TYPE:** \`str | | `is_global` | Whether workspace is global. **TYPE:** \`bool | | `is_restricted` | Whether workspace has restrictions. **TYPE:** \`bool | | `is_visible` | Whether workspace is visible. **TYPE:** \`bool | | `created_iso` | ISO 8601 creation timestamp. **TYPE:** \`str | | `creator_name` | Name of workspace creator. **TYPE:** \`str | Example ``` ws = PublicWorkspace( id=1, name="Main", project_id=12345, is_default=True ) assert ws.is_default is True ``` ### id ``` id: int ``` Workspace identifier. ### name ``` name: str ``` Human-readable workspace name. ### project_id ``` project_id: int ``` Parent project identifier. ### is_default ``` is_default: bool ``` Whether this is the default workspace. ### description ``` description: str | None = None ``` Workspace description, if set. ### is_global ``` is_global: bool | None = None ``` Whether workspace is global. ### is_restricted ``` is_restricted: bool | None = None ``` Whether workspace has restrictions. ### is_visible ``` is_visible: bool | None = None ``` Whether workspace is visible. ### created_iso ``` created_iso: str | None = None ``` ISO 8601 creation timestamp. ### creator_name ``` creator_name: str | None = None ``` Name of workspace creator. ## mixpanel_headless.CursorPagination Bases: `BaseModel` Cursor-based pagination metadata from App API responses. | ATTRIBUTE | DESCRIPTION | | ----------------- | ----------------------------------------------------------- | | `page_size` | Number of items per page. **TYPE:** `int` | | `next_cursor` | Cursor for next page, or None if last page. **TYPE:** \`str | | `previous_cursor` | Cursor for previous page. **TYPE:** \`str | Example ``` pagination = CursorPagination(page_size=100, next_cursor="abc123") assert pagination.next_cursor == "abc123" ``` ### page_size ``` page_size: int ``` Number of items per page. ### next_cursor ``` next_cursor: str | None = None ``` Cursor for next page (None = last page). ### previous_cursor ``` previous_cursor: str | None = None ``` Cursor for previous page. ## mixpanel_headless.PaginatedResponse Bases: `BaseModel`, `Generic[T]` Paginated App API response wrapper. Generic wrapper for paginated responses from the Mixpanel App API. Contains the results list, status, and optional pagination metadata. | ATTRIBUTE | DESCRIPTION | | ------------ | ------------------------------------------------------------------------------------ | | `status` | Response status (typically "ok"). **TYPE:** `str` | | `results` | Page of results. **TYPE:** `list[T]` | | `pagination` | Pagination metadata, or None for single-page responses. **TYPE:** \`CursorPagination | Example ``` response = PaginatedResponse[dict]( status="ok", results=[{"id": 1}], pagination=CursorPagination(page_size=100), ) assert len(response.results) == 1 ``` ### status ``` status: str ``` Response status (typically "ok"). ### results ``` results: list[T] ``` Page of results. ### pagination ``` pagination: CursorPagination | None = None ``` Pagination metadata, or None for single-page responses. ## Insights Query Types Types for `Workspace.query()` β€” typed Insights engine queries with composable metrics, filters, and breakdowns. ## mixpanel_headless.Metric ``` Metric( event: str, math: MathType = "total", property: str | CustomPropertyRef | InlineCustomProperty | None = None, per_user: PerUserAggregation | None = None, percentile_value: int | float | None = None, filters: list[Filter] | None = None, filters_combinator: FiltersCombinator = "all", segment_method: SegmentMethod | None = None, ) ``` Encapsulates a single event to query with its aggregation settings. Used with `Workspace.query()` to specify per-event math, property, per-user aggregation, and filters. Plain event name strings inherit top-level query defaults; Metric objects override them. | ATTRIBUTE | DESCRIPTION | | -------------------- | ------------------------------------------------------------------------------------------------ | | `event` | Mixpanel event name. **TYPE:** `str` | | `math` | Aggregation function. Default: "total". **TYPE:** `MathType` | | `property` | Property for property-based math types (name, ref, or inline). **TYPE:** \`str | | `per_user` | Per-user pre-aggregation (average, total, min, max). **TYPE:** \`PerUserAggregation | | `filters` | Per-metric filters (applied in addition to global where). **TYPE:** \`list[Filter] | | `filters_combinator` | How per-metric filters combine. "all" = AND (default), "any" = OR. **TYPE:** `FiltersCombinator` | Example ``` from mixpanel_headless import Metric # Simple event with defaults m1 = Metric("Login") # With aggregation m2 = Metric("Purchase", math="average", property="amount") # With per-user aggregation m3 = Metric("Purchase", math="total", per_user="average") ``` ### event ``` event: str ``` Mixpanel event name. ### math ``` math: MathType = 'total' ``` Aggregation function. ### property ``` property: str | CustomPropertyRef | InlineCustomProperty | None = None ``` Property for property-based math types (name, ref, or inline). ### per_user ``` per_user: PerUserAggregation | None = None ``` Per-user pre-aggregation type. ### percentile_value ``` percentile_value: int | float | None = None ``` Custom percentile value (e.g. 95 for p95). Required when `math="percentile"`. Ignored for other math types. Maps to `percentile` in bookmark JSON. ### filters ``` filters: list[Filter] | None = None ``` Per-metric filters (list of Filter objects). ### filters_combinator ``` filters_combinator: FiltersCombinator = 'all' ``` How per-metric filters combine (`"all"` = AND, `"any"` = OR). ### segment_method ``` segment_method: SegmentMethod | None = None ``` Segment method for counting qualifying events. Controls how events are counted per user: `"all"` counts every qualifying event (default server behavior), `"first"` counts only the first qualifying event per user. Maps to `segmentMethod` in the bookmark measurement block. ### __post_init__ ``` __post_init__() -> None ``` Validate construction arguments. | RAISES | DESCRIPTION | | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `ValueError` | If event is empty or contains control characters (M1), math requires a property but none is set (M2), or math="percentile" but percentile_value is missing (M3). | Source code in `src/mixpanel_headless/types.py` ``` def __post_init__(self) -> None: """Validate construction arguments. Raises: ValueError: If event is empty or contains control characters (M1), math requires a property but none is set (M2), or math="percentile" but percentile_value is missing (M3). """ _validate_event_name(self.event, "Metric") if self.math in _MATH_REQUIRING_PROPERTY and self.property is None: raise ValueError( f"Metric math={self.math!r} requires a property " f"to be set (e.g., Metric({self.event!r}, math={self.math!r}, " f'property="your_property"))' ) if self.math == "percentile" and self.percentile_value is None: raise ValueError( 'Metric math="percentile" requires percentile_value ' "(e.g., Metric(event, math='percentile', percentile_value=95))" ) # M4: segment_method must be valid if set if self.segment_method is not None: valid_segments = {"all", "first"} if self.segment_method not in valid_segments: raise ValueError( f"Metric segment_method must be one of {sorted(valid_segments)}, " f"got {self.segment_method!r}" ) ``` ## mixpanel_headless.Formula ``` Formula(expression: str, label: str | None = None) ``` A formula expression referencing events by position letter (A, B, C...). Letters map to event positions in the list passed to `Workspace.query()`. A is the first event, B the second, etc. Can be passed as an element of the events list alongside strings and `Metric` objects, or use the top-level `formula` parameter for single-formula convenience. | ATTRIBUTE | DESCRIPTION | | ------------ | -------------------------------------------------------------- | | `expression` | Formula expression, e.g. "(B / A) * 100". **TYPE:** `str` | | `label` | Optional display label for the formula result. **TYPE:** \`str | Example ``` from mixpanel_headless import Formula, Metric # Formula in the events list result = ws.query( [Metric("Signup", math="unique"), Metric("Purchase", math="unique"), Formula("(B / A) * 100", label="Conversion %")], ) # Equivalent using top-level parameter result = ws.query( [Metric("Signup", math="unique"), Metric("Purchase", math="unique")], formula="(B / A) * 100", formula_label="Conversion %", ) ``` ### expression ``` expression: str ``` Formula expression referencing events by letter. ### label ``` label: str | None = None ``` Optional display label for the formula result. ### __post_init__ ``` __post_init__() -> None ``` Validate construction arguments. | RAISES | DESCRIPTION | | ------------ | ----------------------------- | | `ValueError` | If expression is empty (FM1). | Source code in `src/mixpanel_headless/types.py` ``` def __post_init__(self) -> None: """Validate construction arguments. Raises: ValueError: If expression is empty (FM1). """ if not self.expression or not self.expression.strip(): raise ValueError("Formula.expression must be a non-empty string") ``` ## mixpanel_headless.Filter ``` Filter( _property: str | CustomPropertyRef | InlineCustomProperty, _operator: FilterOperator, _value: str | int | float | list[str] | list[int | float] | list[dict[str, Any]] | None, _property_type: FilterPropertyType = "string", _resource_type: Literal["events", "people"] = "events", _date_unit: FilterDateUnit | None = None, _list_item_filters: tuple[Filter, ...] | None = None, _list_item_quantifier: Literal["any", "all"] | None = None, ) ``` Represents a typed filter condition on a property. Constructed exclusively via class methods β€” never instantiated directly. Each class method maps to specific filterType, filterOperator, and filterValue format in the bookmark JSON. Example ``` from mixpanel_headless import Filter f1 = Filter.equals("country", "US") f2 = Filter.greater_than("age", 18) f3 = Filter.between("amount", 10, 100) f4 = Filter.is_set("email") ``` ### __post_init__ ``` __post_init__() -> None ``` Validate Filter mode invariants. Only validates the `list_contains` mode today. Other operator modes rely on classmethod-only validation; expanding this coverage is a separate concern from this PR's list_contains feature. | RAISES | DESCRIPTION | | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `ValueError` | If \_operator == "list_contains" but \_list_item_filters or \_list_item_quantifier is None. List-contains filters must be constructed via Filter.list_contains(...). | Source code in `src/mixpanel_headless/types.py` ``` def __post_init__(self) -> None: """Validate Filter mode invariants. Only validates the ``list_contains`` mode today. Other operator modes rely on classmethod-only validation; expanding this coverage is a separate concern from this PR's list_contains feature. Raises: ValueError: If ``_operator == "list_contains"`` but ``_list_item_filters`` or ``_list_item_quantifier`` is ``None``. List-contains filters must be constructed via ``Filter.list_contains(...)``. """ if self._operator == "list_contains": if self._list_item_filters is None: raise ValueError( "list_contains Filter requires _list_item_filters; " "construct via Filter.list_contains(...)" ) if self._list_item_quantifier is None: raise ValueError( "list_contains Filter requires _list_item_quantifier; " "construct via Filter.list_contains(...)" ) ``` ### equals ``` equals( property: str | CustomPropertyRef | InlineCustomProperty, value: str | list[str], *, resource_type: Literal["events", "people"] = "events", ) -> Filter ``` Create an equality filter. | PARAMETER | DESCRIPTION | | --------------- | ------------------------------------------------------------------------------------------------- | | `property` | Property name, CustomPropertyRef, or InlineCustomProperty. **TYPE:** \`str | | `value` | Value or list of values. **TYPE:** \`str | | `resource_type` | Resource type. Default: "events". **TYPE:** `Literal['events', 'people']` **DEFAULT:** `'events'` | | RETURNS | DESCRIPTION | | -------- | --------------------------- | | `Filter` | Filter for string equality. | Source code in `src/mixpanel_headless/types.py` ``` @classmethod def equals( cls, property: str | CustomPropertyRef | InlineCustomProperty, value: str | list[str], *, resource_type: Literal["events", "people"] = "events", ) -> Filter: """Create an equality filter. Args: property: Property name, CustomPropertyRef, or InlineCustomProperty. value: Value or list of values. resource_type: Resource type. Default: ``"events"``. Returns: Filter for string equality. """ val = [value] if isinstance(value, str) else value return cls( _property=property, _operator="equals", _value=val, _property_type="string", _resource_type=resource_type, ) ``` ### not_equals ``` not_equals( property: str | CustomPropertyRef | InlineCustomProperty, value: str | list[str], *, resource_type: Literal["events", "people"] = "events", ) -> Filter ``` Create a not-equals filter. | PARAMETER | DESCRIPTION | | --------------- | ------------------------------------------------------------------------------------------------- | | `property` | Property name, CustomPropertyRef, or InlineCustomProperty. **TYPE:** \`str | | `value` | Value or list of values. **TYPE:** \`str | | `resource_type` | Resource type. Default: "events". **TYPE:** `Literal['events', 'people']` **DEFAULT:** `'events'` | | RETURNS | DESCRIPTION | | -------- | ----------------------------- | | `Filter` | Filter for string inequality. | Source code in `src/mixpanel_headless/types.py` ``` @classmethod def not_equals( cls, property: str | CustomPropertyRef | InlineCustomProperty, value: str | list[str], *, resource_type: Literal["events", "people"] = "events", ) -> Filter: """Create a not-equals filter. Args: property: Property name, CustomPropertyRef, or InlineCustomProperty. value: Value or list of values. resource_type: Resource type. Default: ``"events"``. Returns: Filter for string inequality. """ val = [value] if isinstance(value, str) else value return cls( _property=property, _operator="does not equal", _value=val, _property_type="string", _resource_type=resource_type, ) ``` ### contains ``` contains( property: str | CustomPropertyRef | InlineCustomProperty, value: str, *, resource_type: Literal["events", "people"] = "events", ) -> Filter ``` Create a contains (substring) filter. | PARAMETER | DESCRIPTION | | --------------- | ------------------------------------------------------------------------------------------------- | | `property` | Property name, CustomPropertyRef, or InlineCustomProperty. **TYPE:** \`str | | `value` | Substring to match. **TYPE:** `str` | | `resource_type` | Resource type. Default: "events". **TYPE:** `Literal['events', 'people']` **DEFAULT:** `'events'` | | RETURNS | DESCRIPTION | | -------- | --------------------------------- | | `Filter` | Filter for substring containment. | Source code in `src/mixpanel_headless/types.py` ``` @classmethod def contains( cls, property: str | CustomPropertyRef | InlineCustomProperty, value: str, *, resource_type: Literal["events", "people"] = "events", ) -> Filter: """Create a contains (substring) filter. Args: property: Property name, CustomPropertyRef, or InlineCustomProperty. value: Substring to match. resource_type: Resource type. Default: ``"events"``. Returns: Filter for substring containment. """ return cls( _property=property, _operator="contains", _value=value, _property_type="string", _resource_type=resource_type, ) ``` ### not_contains ``` not_contains( property: str | CustomPropertyRef | InlineCustomProperty, value: str, *, resource_type: Literal["events", "people"] = "events", ) -> Filter ``` Create a not-contains filter. | PARAMETER | DESCRIPTION | | --------------- | ------------------------------------------------------------------------------------------------- | | `property` | Property name, CustomPropertyRef, or InlineCustomProperty. **TYPE:** \`str | | `value` | Substring that must not match. **TYPE:** `str` | | `resource_type` | Resource type. Default: "events". **TYPE:** `Literal['events', 'people']` **DEFAULT:** `'events'` | | RETURNS | DESCRIPTION | | -------- | ------------------------------------- | | `Filter` | Filter for substring non-containment. | Source code in `src/mixpanel_headless/types.py` ``` @classmethod def not_contains( cls, property: str | CustomPropertyRef | InlineCustomProperty, value: str, *, resource_type: Literal["events", "people"] = "events", ) -> Filter: """Create a not-contains filter. Args: property: Property name, CustomPropertyRef, or InlineCustomProperty. value: Substring that must not match. resource_type: Resource type. Default: ``"events"``. Returns: Filter for substring non-containment. """ return cls( _property=property, _operator="does not contain", _value=value, _property_type="string", _resource_type=resource_type, ) ``` ### greater_than ``` greater_than( property: str | CustomPropertyRef | InlineCustomProperty, value: int | float, *, resource_type: Literal["events", "people"] = "events", ) -> Filter ``` Create a greater-than filter. | PARAMETER | DESCRIPTION | | --------------- | ------------------------------------------------------------------------------------------------- | | `property` | Property name, CustomPropertyRef, or InlineCustomProperty. **TYPE:** \`str | | `value` | Numeric threshold. **TYPE:** \`int | | `resource_type` | Resource type. Default: "events". **TYPE:** `Literal['events', 'people']` **DEFAULT:** `'events'` | | RETURNS | DESCRIPTION | | -------- | -------------------------------- | | `Filter` | Filter for numeric greater-than. | Source code in `src/mixpanel_headless/types.py` ``` @classmethod def greater_than( cls, property: str | CustomPropertyRef | InlineCustomProperty, value: int | float, *, resource_type: Literal["events", "people"] = "events", ) -> Filter: """Create a greater-than filter. Args: property: Property name, CustomPropertyRef, or InlineCustomProperty. value: Numeric threshold. resource_type: Resource type. Default: ``"events"``. Returns: Filter for numeric greater-than. """ return cls( _property=property, _operator="is greater than", _value=value, _property_type="number", _resource_type=resource_type, ) ``` ### less_than ``` less_than( property: str | CustomPropertyRef | InlineCustomProperty, value: int | float, *, resource_type: Literal["events", "people"] = "events", ) -> Filter ``` Create a less-than filter. | PARAMETER | DESCRIPTION | | --------------- | ------------------------------------------------------------------------------------------------- | | `property` | Property name, CustomPropertyRef, or InlineCustomProperty. **TYPE:** \`str | | `value` | Numeric threshold. **TYPE:** \`int | | `resource_type` | Resource type. Default: "events". **TYPE:** `Literal['events', 'people']` **DEFAULT:** `'events'` | | RETURNS | DESCRIPTION | | -------- | ----------------------------- | | `Filter` | Filter for numeric less-than. | Source code in `src/mixpanel_headless/types.py` ``` @classmethod def less_than( cls, property: str | CustomPropertyRef | InlineCustomProperty, value: int | float, *, resource_type: Literal["events", "people"] = "events", ) -> Filter: """Create a less-than filter. Args: property: Property name, CustomPropertyRef, or InlineCustomProperty. value: Numeric threshold. resource_type: Resource type. Default: ``"events"``. Returns: Filter for numeric less-than. """ return cls( _property=property, _operator="is less than", _value=value, _property_type="number", _resource_type=resource_type, ) ``` ### between ``` between( property: str | CustomPropertyRef | InlineCustomProperty, min_val: int | float, max_val: int | float, *, resource_type: Literal["events", "people"] = "events", ) -> Filter ``` Create a between (inclusive range) filter. | PARAMETER | DESCRIPTION | | --------------- | ------------------------------------------------------------------------------------------------- | | `property` | Property name, CustomPropertyRef, or InlineCustomProperty. **TYPE:** \`str | | `min_val` | Minimum value (inclusive). **TYPE:** \`int | | `max_val` | Maximum value (inclusive). **TYPE:** \`int | | `resource_type` | Resource type. Default: "events". **TYPE:** `Literal['events', 'people']` **DEFAULT:** `'events'` | | RETURNS | DESCRIPTION | | -------- | ------------------------- | | `Filter` | Filter for numeric range. | Source code in `src/mixpanel_headless/types.py` ``` @classmethod def between( cls, property: str | CustomPropertyRef | InlineCustomProperty, min_val: int | float, max_val: int | float, *, resource_type: Literal["events", "people"] = "events", ) -> Filter: """Create a between (inclusive range) filter. Args: property: Property name, CustomPropertyRef, or InlineCustomProperty. min_val: Minimum value (inclusive). max_val: Maximum value (inclusive). resource_type: Resource type. Default: ``"events"``. Returns: Filter for numeric range. """ return cls( _property=property, _operator="is between", _value=[min_val, max_val], _property_type="number", _resource_type=resource_type, ) ``` ### not_between ``` not_between( property: str | CustomPropertyRef | InlineCustomProperty, min_val: int | float, max_val: int | float, *, resource_type: Literal["events", "people"] = "events", ) -> Filter ``` Create a not-between (exclusive range) filter. | PARAMETER | DESCRIPTION | | --------------- | ------------------------------------------------------------------------------------------------- | | `property` | Property name, CustomPropertyRef, or InlineCustomProperty. **TYPE:** \`str | | `min_val` | Minimum value (exclusive). **TYPE:** \`int | | `max_val` | Maximum value (exclusive). **TYPE:** \`int | | `resource_type` | Resource type. Default: "events". **TYPE:** `Literal['events', 'people']` **DEFAULT:** `'events'` | | RETURNS | DESCRIPTION | | -------- | -------------------------------------------- | | `Filter` | Filter for numeric values outside the range. | Example ``` f = Filter.not_between("age", 18, 65) ``` Source code in `src/mixpanel_headless/types.py` ```` @classmethod def not_between( cls, property: str | CustomPropertyRef | InlineCustomProperty, min_val: int | float, max_val: int | float, *, resource_type: Literal["events", "people"] = "events", ) -> Filter: """Create a not-between (exclusive range) filter. Args: property: Property name, CustomPropertyRef, or InlineCustomProperty. min_val: Minimum value (exclusive). max_val: Maximum value (exclusive). resource_type: Resource type. Default: ``"events"``. Returns: Filter for numeric values outside the range. Example: ```python f = Filter.not_between("age", 18, 65) ``` """ return cls( _property=property, _operator="not between", _value=[min_val, max_val], _property_type="number", _resource_type=resource_type, ) ```` ### at_least ``` at_least( property: str | CustomPropertyRef | InlineCustomProperty, value: int | float, *, resource_type: Literal["events", "people"] = "events", ) -> Filter ``` Create a greater-than-or-equal filter. | PARAMETER | DESCRIPTION | | --------------- | ------------------------------------------------------------------------------------------------- | | `property` | Property name, CustomPropertyRef, or InlineCustomProperty. **TYPE:** \`str | | `value` | Numeric threshold (inclusive). **TYPE:** \`int | | `resource_type` | Resource type. Default: "events". **TYPE:** `Literal['events', 'people']` **DEFAULT:** `'events'` | | RETURNS | DESCRIPTION | | -------- | ----------------------------------------- | | `Filter` | Filter for numeric greater-than-or-equal. | Example ``` f = Filter.at_least("score", 80) ``` Source code in `src/mixpanel_headless/types.py` ```` @classmethod def at_least( cls, property: str | CustomPropertyRef | InlineCustomProperty, value: int | float, *, resource_type: Literal["events", "people"] = "events", ) -> Filter: """Create a greater-than-or-equal filter. Args: property: Property name, CustomPropertyRef, or InlineCustomProperty. value: Numeric threshold (inclusive). resource_type: Resource type. Default: ``"events"``. Returns: Filter for numeric greater-than-or-equal. Example: ```python f = Filter.at_least("score", 80) ``` """ return cls( _property=property, _operator="is at least", _value=value, _property_type="number", _resource_type=resource_type, ) ```` ### at_most ``` at_most( property: str | CustomPropertyRef | InlineCustomProperty, value: int | float, *, resource_type: Literal["events", "people"] = "events", ) -> Filter ``` Create a less-than-or-equal filter. | PARAMETER | DESCRIPTION | | --------------- | ------------------------------------------------------------------------------------------------- | | `property` | Property name, CustomPropertyRef, or InlineCustomProperty. **TYPE:** \`str | | `value` | Numeric threshold (inclusive). **TYPE:** \`int | | `resource_type` | Resource type. Default: "events". **TYPE:** `Literal['events', 'people']` **DEFAULT:** `'events'` | | RETURNS | DESCRIPTION | | -------- | -------------------------------------- | | `Filter` | Filter for numeric less-than-or-equal. | Example ``` f = Filter.at_most("errors", 5) ``` Source code in `src/mixpanel_headless/types.py` ```` @classmethod def at_most( cls, property: str | CustomPropertyRef | InlineCustomProperty, value: int | float, *, resource_type: Literal["events", "people"] = "events", ) -> Filter: """Create a less-than-or-equal filter. Args: property: Property name, CustomPropertyRef, or InlineCustomProperty. value: Numeric threshold (inclusive). resource_type: Resource type. Default: ``"events"``. Returns: Filter for numeric less-than-or-equal. Example: ```python f = Filter.at_most("errors", 5) ``` """ return cls( _property=property, _operator="is at most", _value=value, _property_type="number", _resource_type=resource_type, ) ```` ### is_set ``` is_set( property: str | CustomPropertyRef | InlineCustomProperty, *, resource_type: Literal["events", "people"] = "events", ) -> Filter ``` Create a property-existence filter. | PARAMETER | DESCRIPTION | | --------------- | ------------------------------------------------------------------------------------------------- | | `property` | Property name, CustomPropertyRef, or InlineCustomProperty. **TYPE:** \`str | | `resource_type` | Resource type. Default: "events". **TYPE:** `Literal['events', 'people']` **DEFAULT:** `'events'` | | RETURNS | DESCRIPTION | | -------- | ------------------------------ | | `Filter` | Filter for property existence. | Source code in `src/mixpanel_headless/types.py` ``` @classmethod def is_set( cls, property: str | CustomPropertyRef | InlineCustomProperty, *, resource_type: Literal["events", "people"] = "events", ) -> Filter: """Create a property-existence filter. Args: property: Property name, CustomPropertyRef, or InlineCustomProperty. resource_type: Resource type. Default: ``"events"``. Returns: Filter for property existence. """ return cls( _property=property, _operator="is set", _value=None, _property_type="string", _resource_type=resource_type, ) ``` ### is_not_set ``` is_not_set( property: str | CustomPropertyRef | InlineCustomProperty, *, resource_type: Literal["events", "people"] = "events", ) -> Filter ``` Create a property-nonexistence filter. | PARAMETER | DESCRIPTION | | --------------- | ------------------------------------------------------------------------------------------------- | | `property` | Property name, CustomPropertyRef, or InlineCustomProperty. **TYPE:** \`str | | `resource_type` | Resource type. Default: "events". **TYPE:** `Literal['events', 'people']` **DEFAULT:** `'events'` | | RETURNS | DESCRIPTION | | -------- | ---------------------------------- | | `Filter` | Filter for property non-existence. | Source code in `src/mixpanel_headless/types.py` ``` @classmethod def is_not_set( cls, property: str | CustomPropertyRef | InlineCustomProperty, *, resource_type: Literal["events", "people"] = "events", ) -> Filter: """Create a property-nonexistence filter. Args: property: Property name, CustomPropertyRef, or InlineCustomProperty. resource_type: Resource type. Default: ``"events"``. Returns: Filter for property non-existence. """ return cls( _property=property, _operator="is not set", _value=None, _property_type="string", _resource_type=resource_type, ) ``` ### starts_with ``` starts_with( property: str | CustomPropertyRef | InlineCustomProperty, prefix: str, *, resource_type: Literal["events", "people"] = "events", ) -> Filter ``` Create a starts-with (prefix match) filter. | PARAMETER | DESCRIPTION | | --------------- | ------------------------------------------------------------------------------------------------- | | `property` | Property name, CustomPropertyRef, or InlineCustomProperty. **TYPE:** \`str | | `prefix` | String prefix to match. **TYPE:** `str` | | `resource_type` | Resource type. Default: "events". **TYPE:** `Literal['events', 'people']` **DEFAULT:** `'events'` | | RETURNS | DESCRIPTION | | -------- | ---------------------------------- | | `Filter` | Filter for string prefix matching. | Example ``` f = Filter.starts_with("url", "https://") ``` Source code in `src/mixpanel_headless/types.py` ```` @classmethod def starts_with( cls, property: str | CustomPropertyRef | InlineCustomProperty, prefix: str, *, resource_type: Literal["events", "people"] = "events", ) -> Filter: """Create a starts-with (prefix match) filter. Args: property: Property name, CustomPropertyRef, or InlineCustomProperty. prefix: String prefix to match. resource_type: Resource type. Default: ``"events"``. Returns: Filter for string prefix matching. Example: ```python f = Filter.starts_with("url", "https://") ``` """ return cls( _property=property, _operator="starts with", _value=prefix, _property_type="string", _resource_type=resource_type, ) ```` ### ends_with ``` ends_with( property: str | CustomPropertyRef | InlineCustomProperty, suffix: str, *, resource_type: Literal["events", "people"] = "events", ) -> Filter ``` Create an ends-with (suffix match) filter. | PARAMETER | DESCRIPTION | | --------------- | ------------------------------------------------------------------------------------------------- | | `property` | Property name, CustomPropertyRef, or InlineCustomProperty. **TYPE:** \`str | | `suffix` | String suffix to match. **TYPE:** `str` | | `resource_type` | Resource type. Default: "events". **TYPE:** `Literal['events', 'people']` **DEFAULT:** `'events'` | | RETURNS | DESCRIPTION | | -------- | ---------------------------------- | | `Filter` | Filter for string suffix matching. | Example ``` f = Filter.ends_with("email", "@example.com") ``` Source code in `src/mixpanel_headless/types.py` ```` @classmethod def ends_with( cls, property: str | CustomPropertyRef | InlineCustomProperty, suffix: str, *, resource_type: Literal["events", "people"] = "events", ) -> Filter: """Create an ends-with (suffix match) filter. Args: property: Property name, CustomPropertyRef, or InlineCustomProperty. suffix: String suffix to match. resource_type: Resource type. Default: ``"events"``. Returns: Filter for string suffix matching. Example: ```python f = Filter.ends_with("email", "@example.com") ``` """ return cls( _property=property, _operator="ends with", _value=suffix, _property_type="string", _resource_type=resource_type, ) ```` ### is_true ``` is_true( property: str | CustomPropertyRef | InlineCustomProperty, *, resource_type: Literal["events", "people"] = "events", ) -> Filter ``` Create a boolean true filter. | PARAMETER | DESCRIPTION | | --------------- | ------------------------------------------------------------------------------------------------- | | `property` | Property name, CustomPropertyRef, or InlineCustomProperty. **TYPE:** \`str | | `resource_type` | Resource type. Default: "events". **TYPE:** `Literal['events', 'people']` **DEFAULT:** `'events'` | | RETURNS | DESCRIPTION | | -------- | ------------------------ | | `Filter` | Filter for boolean true. | Source code in `src/mixpanel_headless/types.py` ``` @classmethod def is_true( cls, property: str | CustomPropertyRef | InlineCustomProperty, *, resource_type: Literal["events", "people"] = "events", ) -> Filter: """Create a boolean true filter. Args: property: Property name, CustomPropertyRef, or InlineCustomProperty. resource_type: Resource type. Default: ``"events"``. Returns: Filter for boolean true. """ return cls( _property=property, _operator="true", _value=None, _property_type="boolean", _resource_type=resource_type, ) ``` ### is_false ``` is_false( property: str | CustomPropertyRef | InlineCustomProperty, *, resource_type: Literal["events", "people"] = "events", ) -> Filter ``` Create a boolean false filter. | PARAMETER | DESCRIPTION | | --------------- | ------------------------------------------------------------------------------------------------- | | `property` | Property name, CustomPropertyRef, or InlineCustomProperty. **TYPE:** \`str | | `resource_type` | Resource type. Default: "events". **TYPE:** `Literal['events', 'people']` **DEFAULT:** `'events'` | | RETURNS | DESCRIPTION | | -------- | ------------------------- | | `Filter` | Filter for boolean false. | Source code in `src/mixpanel_headless/types.py` ``` @classmethod def is_false( cls, property: str | CustomPropertyRef | InlineCustomProperty, *, resource_type: Literal["events", "people"] = "events", ) -> Filter: """Create a boolean false filter. Args: property: Property name, CustomPropertyRef, or InlineCustomProperty. resource_type: Resource type. Default: ``"events"``. Returns: Filter for boolean false. """ return cls( _property=property, _operator="false", _value=None, _property_type="boolean", _resource_type=resource_type, ) ``` ### in_cohort ``` in_cohort(cohort: int | CohortDefinition, name: str | None = None) -> Filter ``` Create a filter restricting to users in a cohort. Accepts either a saved cohort ID (`int`) or an inline `CohortDefinition`. The filter can be passed to `where=` on any query method (`query`, `query_funnel`, `query_retention`, `query_flow`). | PARAMETER | DESCRIPTION | | --------- | ------------------------------------------------------------------------------------------------------------ | | `cohort` | Saved cohort ID (positive integer) or inline CohortDefinition. **TYPE:** \`int | | `name` | Display name for the cohort. Optional for saved cohorts; recommended for inline definitions. **TYPE:** \`str | | RETURNS | DESCRIPTION | | -------- | ---------------------------------------- | | `Filter` | Filter for cohort membership (contains). | | RAISES | DESCRIPTION | | ------------ | ------------------------------------------------------------------------ | | `ValueError` | If cohort ID is not positive (CF1) or name is empty when provided (CF2). | Example ``` from mixpanel_headless import Filter # Saved cohort f = Filter.in_cohort(123, "Power Users") # Inline cohort f = Filter.in_cohort(cohort_def, name="Frequent Buyers") ``` Source code in `src/mixpanel_headless/types.py` ```` @classmethod def in_cohort( cls, cohort: int | CohortDefinition, name: str | None = None, ) -> Filter: """Create a filter restricting to users in a cohort. Accepts either a saved cohort ID (``int``) or an inline ``CohortDefinition``. The filter can be passed to ``where=`` on any query method (``query``, ``query_funnel``, ``query_retention``, ``query_flow``). Args: cohort: Saved cohort ID (positive integer) or inline ``CohortDefinition``. name: Display name for the cohort. Optional for saved cohorts; recommended for inline definitions. Returns: Filter for cohort membership (contains). Raises: ValueError: If cohort ID is not positive (CF1) or name is empty when provided (CF2). Example: ```python from mixpanel_headless import Filter # Saved cohort f = Filter.in_cohort(123, "Power Users") # Inline cohort f = Filter.in_cohort(cohort_def, name="Frequent Buyers") ``` """ return cls._build_cohort_filter(cohort, name, negated=False) ```` ### not_in_cohort ``` not_in_cohort( cohort: int | CohortDefinition, name: str | None = None ) -> Filter ``` Create a filter excluding users in a cohort. Accepts either a saved cohort ID (`int`) or an inline `CohortDefinition`. The filter can be passed to `where=` on any query method. | PARAMETER | DESCRIPTION | | --------- | ------------------------------------------------------------------------------------------------------------ | | `cohort` | Saved cohort ID (positive integer) or inline CohortDefinition. **TYPE:** \`int | | `name` | Display name for the cohort. Optional for saved cohorts; recommended for inline definitions. **TYPE:** \`str | | RETURNS | DESCRIPTION | | -------- | ----------------------------------------------- | | `Filter` | Filter for cohort exclusion (does not contain). | | RAISES | DESCRIPTION | | ------------ | ------------------------------------------------------------------------ | | `ValueError` | If cohort ID is not positive (CF1) or name is empty when provided (CF2). | Example ``` from mixpanel_headless import Filter f = Filter.not_in_cohort(789, "Bots") ``` Source code in `src/mixpanel_headless/types.py` ```` @classmethod def not_in_cohort( cls, cohort: int | CohortDefinition, name: str | None = None, ) -> Filter: """Create a filter excluding users in a cohort. Accepts either a saved cohort ID (``int``) or an inline ``CohortDefinition``. The filter can be passed to ``where=`` on any query method. Args: cohort: Saved cohort ID (positive integer) or inline ``CohortDefinition``. name: Display name for the cohort. Optional for saved cohorts; recommended for inline definitions. Returns: Filter for cohort exclusion (does not contain). Raises: ValueError: If cohort ID is not positive (CF1) or name is empty when provided (CF2). Example: ```python from mixpanel_headless import Filter f = Filter.not_in_cohort(789, "Bots") ``` """ return cls._build_cohort_filter(cohort, name, negated=True) ```` ### on ``` on( property: str | CustomPropertyRef | InlineCustomProperty, date: str, *, resource_type: Literal["events", "people"] = "events", ) -> Filter ``` Create a date equality filter (exact date match). | PARAMETER | DESCRIPTION | | --------------- | ---------------------------------------------------------------------------------------------------- | | `property` | Property name, CustomPropertyRef, or InlineCustomProperty (e.g. "$time", "created"). **TYPE:** \`str | | `date` | Date in YYYY-MM-DD format. **TYPE:** `str` | | `resource_type` | Resource type. Default: "events". **TYPE:** `Literal['events', 'people']` **DEFAULT:** `'events'` | | RETURNS | DESCRIPTION | | -------- | ---------------------------- | | `Filter` | Filter for exact date match. | | RAISES | DESCRIPTION | | ------------ | -------------------------------- | | `ValueError` | If date is not valid YYYY-MM-DD. | Source code in `src/mixpanel_headless/types.py` ``` @classmethod def on( cls, property: str | CustomPropertyRef | InlineCustomProperty, date: str, *, resource_type: Literal["events", "people"] = "events", ) -> Filter: """Create a date equality filter (exact date match). Args: property: Property name, CustomPropertyRef, or InlineCustomProperty (e.g. ``"$time"``, ``"created"``). date: Date in YYYY-MM-DD format. resource_type: Resource type. Default: ``"events"``. Returns: Filter for exact date match. Raises: ValueError: If date is not valid YYYY-MM-DD. """ cls._validate_date(date) return cls( _property=property, _operator="was on", _value=date, _property_type="datetime", _resource_type=resource_type, ) ``` ### not_on ``` not_on( property: str | CustomPropertyRef | InlineCustomProperty, date: str, *, resource_type: Literal["events", "people"] = "events", ) -> Filter ``` Create a date inequality filter (not on date). | PARAMETER | DESCRIPTION | | --------------- | ------------------------------------------------------------------------------------------------- | | `property` | Property name, CustomPropertyRef, or InlineCustomProperty. **TYPE:** \`str | | `date` | Date in YYYY-MM-DD format. **TYPE:** `str` | | `resource_type` | Resource type. Default: "events". **TYPE:** `Literal['events', 'people']` **DEFAULT:** `'events'` | | RETURNS | DESCRIPTION | | -------- | --------------------------- | | `Filter` | Filter for date inequality. | | RAISES | DESCRIPTION | | ------------ | -------------------------------- | | `ValueError` | If date is not valid YYYY-MM-DD. | Source code in `src/mixpanel_headless/types.py` ``` @classmethod def not_on( cls, property: str | CustomPropertyRef | InlineCustomProperty, date: str, *, resource_type: Literal["events", "people"] = "events", ) -> Filter: """Create a date inequality filter (not on date). Args: property: Property name, CustomPropertyRef, or InlineCustomProperty. date: Date in YYYY-MM-DD format. resource_type: Resource type. Default: ``"events"``. Returns: Filter for date inequality. Raises: ValueError: If date is not valid YYYY-MM-DD. """ cls._validate_date(date) return cls( _property=property, _operator="was not on", _value=date, _property_type="datetime", _resource_type=resource_type, ) ``` ### before ``` before( property: str | CustomPropertyRef | InlineCustomProperty, date: str, *, resource_type: Literal["events", "people"] = "events", ) -> Filter ``` Create a date before filter. | PARAMETER | DESCRIPTION | | --------------- | ------------------------------------------------------------------------------------------------- | | `property` | Property name, CustomPropertyRef, or InlineCustomProperty. **TYPE:** \`str | | `date` | Date in YYYY-MM-DD format. **TYPE:** `str` | | `resource_type` | Resource type. Default: "events". **TYPE:** `Literal['events', 'people']` **DEFAULT:** `'events'` | | RETURNS | DESCRIPTION | | -------- | ------------------------------------------- | | `Filter` | Filter for dates before the specified date. | | RAISES | DESCRIPTION | | ------------ | -------------------------------- | | `ValueError` | If date is not valid YYYY-MM-DD. | Source code in `src/mixpanel_headless/types.py` ``` @classmethod def before( cls, property: str | CustomPropertyRef | InlineCustomProperty, date: str, *, resource_type: Literal["events", "people"] = "events", ) -> Filter: """Create a date before filter. Args: property: Property name, CustomPropertyRef, or InlineCustomProperty. date: Date in YYYY-MM-DD format. resource_type: Resource type. Default: ``"events"``. Returns: Filter for dates before the specified date. Raises: ValueError: If date is not valid YYYY-MM-DD. """ cls._validate_date(date) return cls( _property=property, _operator="was before", _value=date, _property_type="datetime", _resource_type=resource_type, ) ``` ### since ``` since( property: str | CustomPropertyRef | InlineCustomProperty, date: str, *, resource_type: Literal["events", "people"] = "events", ) -> Filter ``` Create a date since filter (from date onward). | PARAMETER | DESCRIPTION | | --------------- | ------------------------------------------------------------------------------------------------- | | `property` | Property name, CustomPropertyRef, or InlineCustomProperty. **TYPE:** \`str | | `date` | Date in YYYY-MM-DD format. **TYPE:** `str` | | `resource_type` | Resource type. Default: "events". **TYPE:** `Literal['events', 'people']` **DEFAULT:** `'events'` | | RETURNS | DESCRIPTION | | -------- | ------------------------------------------------ | | `Filter` | Filter for dates on or after the specified date. | | RAISES | DESCRIPTION | | ------------ | -------------------------------- | | `ValueError` | If date is not valid YYYY-MM-DD. | Source code in `src/mixpanel_headless/types.py` ``` @classmethod def since( cls, property: str | CustomPropertyRef | InlineCustomProperty, date: str, *, resource_type: Literal["events", "people"] = "events", ) -> Filter: """Create a date since filter (from date onward). Args: property: Property name, CustomPropertyRef, or InlineCustomProperty. date: Date in YYYY-MM-DD format. resource_type: Resource type. Default: ``"events"``. Returns: Filter for dates on or after the specified date. Raises: ValueError: If date is not valid YYYY-MM-DD. """ cls._validate_date(date) return cls( _property=property, _operator="was since", _value=date, _property_type="datetime", _resource_type=resource_type, ) ``` ### in_the_last ``` in_the_last( property: str | CustomPropertyRef | InlineCustomProperty, quantity: int, date_unit: FilterDateUnit, *, resource_type: Literal["events", "people"] = "events", ) -> Filter ``` Create a relative date filter (in the last N units). | PARAMETER | DESCRIPTION | | --------------- | ------------------------------------------------------------------------------------------------- | | `property` | Property name, CustomPropertyRef, or InlineCustomProperty. **TYPE:** \`str | | `quantity` | Number of time units (must be positive). **TYPE:** `int` | | `date_unit` | Time unit ("hour", "day", "week", "month"). **TYPE:** `FilterDateUnit` | | `resource_type` | Resource type. Default: "events". **TYPE:** `Literal['events', 'people']` **DEFAULT:** `'events'` | | RETURNS | DESCRIPTION | | -------- | ------------------------------------------ | | `Filter` | Filter for events within the last N units. | | RAISES | DESCRIPTION | | ------------ | ---------------------------- | | `ValueError` | If quantity is not positive. | Source code in `src/mixpanel_headless/types.py` ``` @classmethod def in_the_last( cls, property: str | CustomPropertyRef | InlineCustomProperty, quantity: int, date_unit: FilterDateUnit, *, resource_type: Literal["events", "people"] = "events", ) -> Filter: """Create a relative date filter (in the last N units). Args: property: Property name, CustomPropertyRef, or InlineCustomProperty. quantity: Number of time units (must be positive). date_unit: Time unit (``"hour"``, ``"day"``, ``"week"``, ``"month"``). resource_type: Resource type. Default: ``"events"``. Returns: Filter for events within the last N units. Raises: ValueError: If quantity is not positive. """ if quantity <= 0: raise ValueError(f"quantity must be a positive integer (got {quantity})") return cls( _property=property, _operator="was in the", _value=quantity, _property_type="datetime", _resource_type=resource_type, _date_unit=date_unit, ) ``` ### not_in_the_last ``` not_in_the_last( property: str | CustomPropertyRef | InlineCustomProperty, quantity: int, date_unit: FilterDateUnit, *, resource_type: Literal["events", "people"] = "events", ) -> Filter ``` Create a relative date exclusion filter (not in the last N units). | PARAMETER | DESCRIPTION | | --------------- | ------------------------------------------------------------------------------------------------- | | `property` | Property name, CustomPropertyRef, or InlineCustomProperty. **TYPE:** \`str | | `quantity` | Number of time units (must be positive). **TYPE:** `int` | | `date_unit` | Time unit ("hour", "day", "week", "month"). **TYPE:** `FilterDateUnit` | | `resource_type` | Resource type. Default: "events". **TYPE:** `Literal['events', 'people']` **DEFAULT:** `'events'` | | RETURNS | DESCRIPTION | | -------- | ---------------------------------------------- | | `Filter` | Filter for events NOT within the last N units. | | RAISES | DESCRIPTION | | ------------ | ---------------------------- | | `ValueError` | If quantity is not positive. | Source code in `src/mixpanel_headless/types.py` ``` @classmethod def not_in_the_last( cls, property: str | CustomPropertyRef | InlineCustomProperty, quantity: int, date_unit: FilterDateUnit, *, resource_type: Literal["events", "people"] = "events", ) -> Filter: """Create a relative date exclusion filter (not in the last N units). Args: property: Property name, CustomPropertyRef, or InlineCustomProperty. quantity: Number of time units (must be positive). date_unit: Time unit (``"hour"``, ``"day"``, ``"week"``, ``"month"``). resource_type: Resource type. Default: ``"events"``. Returns: Filter for events NOT within the last N units. Raises: ValueError: If quantity is not positive. """ if quantity <= 0: raise ValueError(f"quantity must be a positive integer (got {quantity})") return cls( _property=property, _operator="was not in the", _value=quantity, _property_type="datetime", _resource_type=resource_type, _date_unit=date_unit, ) ``` ### date_between ``` date_between( property: str | CustomPropertyRef | InlineCustomProperty, from_date: str, to_date: str, *, resource_type: Literal["events", "people"] = "events", ) -> Filter ``` Create a date range filter (between two dates, inclusive). | PARAMETER | DESCRIPTION | | --------------- | ------------------------------------------------------------------------------------------------- | | `property` | Property name, CustomPropertyRef, or InlineCustomProperty. **TYPE:** \`str | | `from_date` | Start date in YYYY-MM-DD format. **TYPE:** `str` | | `to_date` | End date in YYYY-MM-DD format. **TYPE:** `str` | | `resource_type` | Resource type. Default: "events". **TYPE:** `Literal['events', 'people']` **DEFAULT:** `'events'` | | RETURNS | DESCRIPTION | | -------- | ---------------------------------- | | `Filter` | Filter for dates within the range. | | RAISES | DESCRIPTION | | ------------ | ---------------------------------------------------------------- | | `ValueError` | If dates are not valid YYYY-MM-DD or from_date is after to_date. | Source code in `src/mixpanel_headless/types.py` ``` @classmethod def date_between( cls, property: str | CustomPropertyRef | InlineCustomProperty, from_date: str, to_date: str, *, resource_type: Literal["events", "people"] = "events", ) -> Filter: """Create a date range filter (between two dates, inclusive). Args: property: Property name, CustomPropertyRef, or InlineCustomProperty. from_date: Start date in YYYY-MM-DD format. to_date: End date in YYYY-MM-DD format. resource_type: Resource type. Default: ``"events"``. Returns: Filter for dates within the range. Raises: ValueError: If dates are not valid YYYY-MM-DD or from_date is after to_date. """ from_parsed = cls._validate_date(from_date) to_parsed = cls._validate_date(to_date) if from_parsed > to_parsed: raise ValueError( f"from_date must be before to_date (got '{from_date}' > '{to_date}')" ) return cls( _property=property, _operator="was between", _value=[from_date, to_date], _property_type="datetime", _resource_type=resource_type, ) ``` ### date_not_between ``` date_not_between( property: str | CustomPropertyRef | InlineCustomProperty, from_date: str, to_date: str, *, resource_type: Literal["events", "people"] = "events", ) -> Filter ``` Create a date exclusion range filter (not between two dates). | PARAMETER | DESCRIPTION | | --------------- | ------------------------------------------------------------------------------------------------- | | `property` | Property name, CustomPropertyRef, or InlineCustomProperty. **TYPE:** \`str | | `from_date` | Start date in YYYY-MM-DD format. **TYPE:** `str` | | `to_date` | End date in YYYY-MM-DD format. **TYPE:** `str` | | `resource_type` | Resource type. Default: "events". **TYPE:** `Literal['events', 'people']` **DEFAULT:** `'events'` | | RETURNS | DESCRIPTION | | -------- | ----------------------------------- | | `Filter` | Filter for dates outside the range. | | RAISES | DESCRIPTION | | ------------ | ---------------------------------------------------------------- | | `ValueError` | If dates are not valid YYYY-MM-DD or from_date is after to_date. | Example ``` f = Filter.date_not_between("created", "2024-01-01", "2024-06-30") ``` Source code in `src/mixpanel_headless/types.py` ```` @classmethod def date_not_between( cls, property: str | CustomPropertyRef | InlineCustomProperty, from_date: str, to_date: str, *, resource_type: Literal["events", "people"] = "events", ) -> Filter: """Create a date exclusion range filter (not between two dates). Args: property: Property name, CustomPropertyRef, or InlineCustomProperty. from_date: Start date in YYYY-MM-DD format. to_date: End date in YYYY-MM-DD format. resource_type: Resource type. Default: ``"events"``. Returns: Filter for dates outside the range. Raises: ValueError: If dates are not valid YYYY-MM-DD or from_date is after to_date. Example: ```python f = Filter.date_not_between("created", "2024-01-01", "2024-06-30") ``` """ from_parsed = cls._validate_date(from_date) to_parsed = cls._validate_date(to_date) if from_parsed > to_parsed: raise ValueError( f"from_date must be before to_date (got '{from_date}' > '{to_date}')" ) return cls( _property=property, _operator="was not between", _value=[from_date, to_date], _property_type="datetime", _resource_type=resource_type, ) ```` ### in_the_next ``` in_the_next( property: str | CustomPropertyRef | InlineCustomProperty, quantity: int, date_unit: FilterDateUnit, *, resource_type: Literal["events", "people"] = "events", ) -> Filter ``` Create a relative date filter (in the next N units). | PARAMETER | DESCRIPTION | | --------------- | ------------------------------------------------------------------------------------------------- | | `property` | Property name, CustomPropertyRef, or InlineCustomProperty. **TYPE:** \`str | | `quantity` | Number of time units (must be positive). **TYPE:** `int` | | `date_unit` | Time unit ("hour", "day", "week", "month"). **TYPE:** `FilterDateUnit` | | `resource_type` | Resource type. Default: "events". **TYPE:** `Literal['events', 'people']` **DEFAULT:** `'events'` | | RETURNS | DESCRIPTION | | -------- | ------------------------------------------ | | `Filter` | Filter for events within the next N units. | | RAISES | DESCRIPTION | | ------------ | ---------------------------- | | `ValueError` | If quantity is not positive. | Example ``` f = Filter.in_the_next("expires", 7, "day") ``` Source code in `src/mixpanel_headless/types.py` ```` @classmethod def in_the_next( cls, property: str | CustomPropertyRef | InlineCustomProperty, quantity: int, date_unit: FilterDateUnit, *, resource_type: Literal["events", "people"] = "events", ) -> Filter: """Create a relative date filter (in the next N units). Args: property: Property name, CustomPropertyRef, or InlineCustomProperty. quantity: Number of time units (must be positive). date_unit: Time unit (``"hour"``, ``"day"``, ``"week"``, ``"month"``). resource_type: Resource type. Default: ``"events"``. Returns: Filter for events within the next N units. Raises: ValueError: If quantity is not positive. Example: ```python f = Filter.in_the_next("expires", 7, "day") ``` """ if quantity <= 0: raise ValueError(f"quantity must be a positive integer (got {quantity})") return cls( _property=property, _operator="was in the next", _value=quantity, _property_type="datetime", _resource_type=resource_type, _date_unit=date_unit, ) ```` ### list_contains ``` list_contains( property: str, *item_filters: Filter, quantifier: Literal["any", "all"] = "any", resource_type: Literal["events", "people"] = "events", **equals: str | list[str], ) -> Filter ``` Match events whose list-of-object property contains items satisfying inner conditions. Used to filter on subproperties of objects nested inside a list property (e.g. `cart` is a list of `{"Brand": str, "Category": str, "Price": int}`). Each inner condition is evaluated per-item; the `quantifier` controls whether at least one item (`"any"`, the default) or every item (`"all"`) must satisfy all inner conditions. Two ways to specify inner conditions: - **Keyword shorthand** for the common equality case: `Filter.list_contains("cart", Brand="nike", Category="hats")`. Inner equality filters inherit the outer `resource_type`. - **Explicit Filter instances** for any wire-format operator: `Filter.list_contains("cart", Filter.equals("Brand", "nike"), Filter.greater_than("Price", 50))`. Each inner Filter carries its own `resource_type` from its own factory call β€” pass `resource_type=` explicitly on each inner factory if you want them to match the outer. Mixing the two shapes in one call raises `ValueError`. | PARAMETER | DESCRIPTION | | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `property` | Name of the list-of-object property to filter on. **TYPE:** `str` | | `*item_filters` | Inner Filter instances applied per list item. Mutually exclusive with \*\*equals. **TYPE:** `Filter` **DEFAULT:** `()` | | `quantifier` | "any" (β‰₯1 item must match all inner conditions) or "all" (every item must). Default: "any". **TYPE:** `Literal['any', 'all']` **DEFAULT:** `'any'` | | `resource_type` | Resource type. Default: "events". **TYPE:** `Literal['events', 'people']` **DEFAULT:** `'events'` | | `**equals` | Keyword shorthand β€” each key=value becomes Filter.equals(key, value, resource_type=resource_type). Mutually exclusive with \*item_filters. Values must be str or list[str]; keys must be non-empty. **TYPE:** \`str | | RETURNS | DESCRIPTION | | -------- | -------------------------------------------------------- | | `Filter` | Filter that emits the listItemFilters bookmark structure | | `Filter` | on serialization. | | RAISES | DESCRIPTION | | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `ValueError` | If both \*item_filters and \*\*equals are provided, if quantifier is not "any" or "all", if a kwarg key is empty, if no inner conditions are given, or if any inner filter is itself a list_contains (nesting is not supported). | | `TypeError` | If a \*\*equals value is not str or list[str] (the wire format only supports string equality; numeric/boolean comparisons require explicit Filter.equals(...) / Filter.greater_than(...) positional inner filters). | Example ``` from mixpanel_headless import Filter # Cart contains a nike-branded hat f1 = Filter.list_contains("cart", Brand="nike", Category="hats") # Every cart item costs more than $50 f2 = Filter.list_contains( "cart", Filter.greater_than("Price", 50), quantifier="all", ) ``` Source code in `src/mixpanel_headless/types.py` ```` @classmethod def list_contains( cls, property: str, *item_filters: Filter, quantifier: Literal["any", "all"] = "any", resource_type: Literal["events", "people"] = "events", **equals: str | list[str], ) -> Filter: """Match events whose list-of-object property contains items satisfying inner conditions. Used to filter on subproperties of objects nested inside a list property (e.g. ``cart`` is a list of ``{"Brand": str, "Category": str, "Price": int}``). Each inner condition is evaluated per-item; the ``quantifier`` controls whether at least one item (``"any"``, the default) or every item (``"all"``) must satisfy all inner conditions. Two ways to specify inner conditions: - **Keyword shorthand** for the common equality case: ``Filter.list_contains("cart", Brand="nike", Category="hats")``. Inner equality filters inherit the outer ``resource_type``. - **Explicit Filter instances** for any wire-format operator: ``Filter.list_contains("cart", Filter.equals("Brand", "nike"), Filter.greater_than("Price", 50))``. Each inner Filter carries its own ``resource_type`` from its own factory call β€” pass ``resource_type=`` explicitly on each inner factory if you want them to match the outer. Mixing the two shapes in one call raises ``ValueError``. Args: property: Name of the list-of-object property to filter on. *item_filters: Inner ``Filter`` instances applied per list item. Mutually exclusive with ``**equals``. quantifier: ``"any"`` (β‰₯1 item must match all inner conditions) or ``"all"`` (every item must). Default: ``"any"``. resource_type: Resource type. Default: ``"events"``. **equals: Keyword shorthand β€” each ``key=value`` becomes ``Filter.equals(key, value, resource_type=resource_type)``. Mutually exclusive with ``*item_filters``. Values must be ``str`` or ``list[str]``; keys must be non-empty. Returns: Filter that emits the ``listItemFilters`` bookmark structure on serialization. Raises: ValueError: If both ``*item_filters`` and ``**equals`` are provided, if ``quantifier`` is not ``"any"`` or ``"all"``, if a kwarg key is empty, if no inner conditions are given, or if any inner filter is itself a ``list_contains`` (nesting is not supported). TypeError: If a ``**equals`` value is not ``str`` or ``list[str]`` (the wire format only supports string equality; numeric/boolean comparisons require explicit ``Filter.equals(...)`` / ``Filter.greater_than(...)`` positional inner filters). Example: ```python from mixpanel_headless import Filter # Cart contains a nike-branded hat f1 = Filter.list_contains("cart", Brand="nike", Category="hats") # Every cart item costs more than $50 f2 = Filter.list_contains( "cart", Filter.greater_than("Price", 50), quantifier="all", ) ``` """ if item_filters and equals: raise ValueError( "Filter.list_contains: pass either positional Filter instances " "OR keyword equals shorthand, not both" ) if quantifier not in ("any", "all"): raise ValueError( f"Filter.list_contains quantifier must be 'any' or 'all', " f"got {quantifier!r}" ) for k, v in equals.items(): if not k.strip(): raise ValueError( "Filter.list_contains: kwarg keys must be non-empty strings" ) if not isinstance(v, (str, list)): raise TypeError( f"Filter.list_contains kwarg {k!r}: value must be str or " f"list[str], got {type(v).__name__}" ) sub_filters: tuple[Filter, ...] = ( tuple(item_filters) if item_filters else tuple( cls.equals(k, v, resource_type=resource_type) for k, v in equals.items() ) ) if not sub_filters: raise ValueError( "Filter.list_contains requires at least one inner condition" ) for sub in sub_filters: if sub._operator == "list_contains": raise ValueError( "Filter.list_contains does not support nested list_contains filters" ) return cls( _property=property, _operator="list_contains", _value=None, _property_type="object", _resource_type=resource_type, _list_item_filters=sub_filters, _list_item_quantifier=quantifier, ) ```` ## mixpanel_headless.GroupBy ``` GroupBy( property: str | CustomPropertyRef | InlineCustomProperty, property_type: CustomPropertyType = "string", bucket_size: int | float | None = None, bucket_min: int | float | None = None, bucket_max: int | float | None = None, _list_item_mode: ListItemGroupMode | None = None, ) ``` Specifies a property breakdown with optional numeric bucketing. Used with `Workspace.query()` to break down results by property values. String properties are broken down by distinct values; numeric properties can be bucketed into ranges. | ATTRIBUTE | DESCRIPTION | | --------------- | ---------------------------------------------------------------------------- | | `property` | Property to break down by (name, ref, or inline). **TYPE:** \`str | | `property_type` | Data type of the property. Default: "string". **TYPE:** `CustomPropertyType` | | `bucket_size` | Bucket width for numeric properties. **TYPE:** \`int | | `bucket_min` | Minimum value for numeric buckets. **TYPE:** \`int | | `bucket_max` | Maximum value for numeric buckets. **TYPE:** \`int | Example ``` from mixpanel_headless import GroupBy # String breakdown g1 = GroupBy("country") # Numeric bucketed breakdown g2 = GroupBy( "revenue", property_type="number", bucket_size=50, bucket_min=0, bucket_max=500, ) ``` ### property ``` property: str | CustomPropertyRef | InlineCustomProperty ``` Property to break down by (name, ref, or inline). ### property_type ``` property_type: CustomPropertyType = 'string' ``` Data type of the property. One of the four scalar types. Note: list-item breakdowns set `_list_item_mode` instead β€” the wire builder hardcodes `propertyType: "object"` for that branch independently of this field. ### bucket_size ``` bucket_size: int | float | None = None ``` Bucket width for numeric properties. ### bucket_min ``` bucket_min: int | float | None = None ``` Minimum value for numeric buckets. ### bucket_max ``` bucket_max: int | float | None = None ``` Maximum value for numeric buckets. ### __post_init__ ``` __post_init__() -> None ``` Validate construction arguments. | RAISES | DESCRIPTION | | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `ValueError` | If property is an empty string (GB1), bucket_size is not positive (GB2), bucket_min >= bucket_max (GB3), \_list_item_mode is combined with bucketing (GB4), or \_list_item_mode is set but property is not a plain str (GB5). | Source code in `src/mixpanel_headless/types.py` ``` def __post_init__(self) -> None: """Validate construction arguments. Raises: ValueError: If property is an empty string (GB1), bucket_size is not positive (GB2), bucket_min >= bucket_max (GB3), ``_list_item_mode`` is combined with bucketing (GB4), or ``_list_item_mode`` is set but ``property`` is not a plain ``str`` (GB5). """ if isinstance(self.property, str) and not self.property.strip(): raise ValueError("GroupBy.property must be a non-empty string") if self.bucket_size is not None and self.bucket_size <= 0: raise ValueError( f"GroupBy.bucket_size must be positive, got {self.bucket_size}" ) if ( self.bucket_min is not None and self.bucket_max is not None and self.bucket_min >= self.bucket_max ): raise ValueError( f"GroupBy.bucket_min ({self.bucket_min}) must be less than " f"bucket_max ({self.bucket_max})" ) if self._list_item_mode is not None: if any( b is not None for b in (self.bucket_size, self.bucket_min, self.bucket_max) ): raise ValueError("GroupBy.list_item is incompatible with bucketing") if not isinstance(self.property, str): raise ValueError( "GroupBy.list_item requires property to be a plain str, " f"got {type(self.property).__name__}" ) ``` ### list_item ``` list_item( property: str, sub: str, *, sub_type: CustomPropertyType = "string" ) -> GroupBy ``` Break down by a subproperty of objects inside a list property. Mirrors the Mixpanel UI's `cart.Brand` / `cart.Category` breakdown for list-of-object properties (e.g. `cart` is a list of `{"Brand": str, "Category": str, "Price": int}` items). Each list item contributes one count per distinct subproperty value it carries. | PARAMETER | DESCRIPTION | | ---------- | ------------------------------------------------------------------------------------------------------- | | `property` | Name of the list-of-object property. **TYPE:** `str` | | `sub` | Subproperty name to break down by. **TYPE:** `str` | | `sub_type` | Data type of the subproperty. Default: "string". **TYPE:** `CustomPropertyType` **DEFAULT:** `'string'` | | RETURNS | DESCRIPTION | | --------- | ------------------------------------------------- | | `GroupBy` | GroupBy whose serialization emits a listItemGroup | | `GroupBy` | structure in the bookmark JSON. | | RAISES | DESCRIPTION | | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `ValueError` | If sub is empty after stripping (via ListItemGroupMode.__post_init__), if sub_type is not one of the four CustomPropertyType values, or if any GroupBy.__post_init__ invariant fails (see :meth:__post_init__ Raises section). | Example ``` from mixpanel_headless import GroupBy # Break down Cart Viewed events by cart.Brand g1 = GroupBy.list_item("cart", "Brand") # Break down by a numeric subproperty g2 = GroupBy.list_item("cart", "Price", sub_type="number") ``` Source code in `src/mixpanel_headless/types.py` ```` @classmethod def list_item( cls, property: str, sub: str, *, sub_type: CustomPropertyType = "string", ) -> GroupBy: """Break down by a subproperty of objects inside a list property. Mirrors the Mixpanel UI's ``cart.Brand`` / ``cart.Category`` breakdown for list-of-object properties (e.g. ``cart`` is a list of ``{"Brand": str, "Category": str, "Price": int}`` items). Each list item contributes one count per distinct subproperty value it carries. Args: property: Name of the list-of-object property. sub: Subproperty name to break down by. sub_type: Data type of the subproperty. Default: ``"string"``. Returns: ``GroupBy`` whose serialization emits a ``listItemGroup`` structure in the bookmark JSON. Raises: ValueError: If ``sub`` is empty after stripping (via ``ListItemGroupMode.__post_init__``), if ``sub_type`` is not one of the four ``CustomPropertyType`` values, or if any ``GroupBy.__post_init__`` invariant fails (see :meth:`__post_init__` Raises section). Example: ```python from mixpanel_headless import GroupBy # Break down Cart Viewed events by cart.Brand g1 = GroupBy.list_item("cart", "Brand") # Break down by a numeric subproperty g2 = GroupBy.list_item("cart", "Price", sub_type="number") ``` """ return cls( property=property, _list_item_mode=ListItemGroupMode(sub=sub, sub_type=sub_type), ) ```` ## mixpanel_headless.ListItemGroupMode ``` ListItemGroupMode(sub: str, sub_type: CustomPropertyType) ``` Discriminator for `GroupBy.list_item` β€” sub-property name + scalar type. Pairs the subproperty name with its inferred scalar type so they cannot be set independently. Used as the optional `_list_item_mode` field on `GroupBy`; presence of this field marks a GroupBy as a list-item breakdown. | ATTRIBUTE | DESCRIPTION | | ---------- | ------------------------------------------------------------------------------------------------ | | `sub` | Subproperty name (must be non-empty after stripping). **TYPE:** `str` | | `sub_type` | Subproperty data type. One of the four CustomPropertyType values. **TYPE:** `CustomPropertyType` | Example ``` from mixpanel_headless import GroupBy, ListItemGroupMode # Constructed indirectly via the classmethod (preferred) g = GroupBy.list_item("cart", "Brand") assert g._list_item_mode == ListItemGroupMode(sub="Brand", sub_type="string") ``` ### sub ``` sub: str ``` Subproperty name as it appears inside each object. ### sub_type ``` sub_type: CustomPropertyType ``` Subproperty data type, matching :data:`CustomPropertyType`. ### __post_init__ ``` __post_init__() -> None ``` Validate sub is non-empty and sub_type is a known scalar type. | RAISES | DESCRIPTION | | ------------ | --------------------------------------------------------------------------------------------- | | `ValueError` | If sub is empty after stripping or sub_type is not one of the four CustomPropertyType values. | Source code in `src/mixpanel_headless/types.py` ``` def __post_init__(self) -> None: """Validate sub is non-empty and sub_type is a known scalar type. Raises: ValueError: If ``sub`` is empty after stripping or ``sub_type`` is not one of the four ``CustomPropertyType`` values. """ if not self.sub.strip(): raise ValueError("ListItemGroupMode.sub must be a non-empty string") if self.sub_type not in ("string", "number", "boolean", "datetime"): raise ValueError( "ListItemGroupMode.sub_type must be one of " "'string'/'number'/'boolean'/'datetime', " f"got {self.sub_type!r}" ) ``` ## mixpanel_headless.QueryResult ``` QueryResult( computed_at: str, from_date: str, to_date: str, headers: list[str] = list(), series: dict[str, Any] = dict(), params: dict[str, Any] = dict(), meta: dict[str, Any] = dict(), *, _df_cache: DataFrame | None = None, ) ``` Bases: `ResultWithDataFrame` Structured output from a Workspace.query() execution. Contains the query response data with lazy DataFrame conversion. The series structure varies by query mode: - Timeseries: `{metric_name: {date_string: value}}` - Total: `{metric_name: {"all": value}}` | ATTRIBUTE | DESCRIPTION | | ------------- | --------------------------------------------------------------------------------------------- | | `computed_at` | When the query was computed (ISO format). **TYPE:** `str` | | `from_date` | Effective start date from response. **TYPE:** `str` | | `to_date` | Effective end date from response. **TYPE:** `str` | | `headers` | Column headers from the insights response. **TYPE:** `list[str]` | | `series` | Query result data (structure varies by mode). **TYPE:** `dict[str, Any]` | | `params` | Generated bookmark params sent to API (for debugging/persistence). **TYPE:** `dict[str, Any]` | | `meta` | Response metadata (sampling factor, limits hit). **TYPE:** `dict[str, Any]` | Example ``` result = ws.query("Login", math="unique", last=7) # DataFrame access print(result.df.head()) # Inspect generated params print(result.params) # Save as a report ws.create_bookmark(CreateBookmarkParams( name="Login Uniques (7d)", bookmark_type="insights", params=result.params, )) ``` ### computed_at ``` computed_at: str ``` When the query was computed (ISO format). ### from_date ``` from_date: str ``` Effective start date from response. ### to_date ``` to_date: str ``` Effective end date from response. ### headers ``` headers: list[str] = field(default_factory=list) ``` Column headers from the insights response. ### series ``` series: dict[str, Any] = field(default_factory=dict) ``` Query result data. For timeseries: `{metric_name: {date_string: value}}` For total: `{metric_name: {"all": value}}` ### params ``` params: dict[str, Any] = field(default_factory=dict) ``` Generated bookmark params sent to API (for debugging/persistence). ### meta ``` meta: dict[str, Any] = field(default_factory=dict) ``` Response metadata. Conforms to :class:`QueryMeta` (sampling_factor, is_cached, computation_time, query_id). ### df ``` df: DataFrame ``` Convert to DataFrame. For timeseries mode: columns are `date`, `event`, `count`. For total mode: columns are `event`, `count`. For segmented timeseries (with `group_by`): columns are `date`, `event`, `segment`, `count`. For segmented total (with `group_by`): columns are `event`, `segment`, `count`. | RETURNS | DESCRIPTION | | ----------- | ------------------------------------------------------------- | | `DataFrame` | Normalized DataFrame with one row per (date, metric, segment) | | `DataFrame` | combination. Segmented responses are detected automatically | | `DataFrame` | by checking whether inner values are dicts (segment nesting) | | `DataFrame` | or scalars (flat response). | ### to_dict ``` to_dict() -> dict[str, Any] ``` Serialize for JSON output. | RETURNS | DESCRIPTION | | ---------------- | --------------------------------------- | | `dict[str, Any]` | Dictionary with all QueryResult fields. | Source code in `src/mixpanel_headless/types.py` ``` def to_dict(self) -> dict[str, Any]: """Serialize for JSON output. Returns: Dictionary with all QueryResult fields. """ return { "computed_at": self.computed_at, "from_date": self.from_date, "to_date": self.to_date, "headers": self.headers, "series": self.series, "params": self.params, "meta": self.meta, } ``` ## Cohort Query Types Types for cohort-scoped queries β€” filter by cohort, break down by cohort membership, or track cohort size as a metric across all query engines. ## mixpanel_headless.CohortBreakdown ``` CohortBreakdown( cohort: int | CohortDefinition, name: str | None = None, include_negated: bool = True, ) ``` Break down query results by cohort membership. Represents a cohort-based breakdown dimension for use in the `group_by=` parameter of `query()`, `query_funnel()`, and `query_retention()`. Accepts either a saved cohort ID (`int`) or an inline `CohortDefinition`. When `include_negated=True` (default), both "In Cohort" and "Not In Cohort" segments are shown. | ATTRIBUTE | DESCRIPTION | | ----------------- | --------------------------------------------------------------------------------------------- | | `cohort` | Saved cohort ID (positive integer) or inline CohortDefinition. **TYPE:** \`int | | `name` | Display name. Optional for saved cohorts; recommended for inline definitions. **TYPE:** \`str | | `include_negated` | Whether to include a "Not In" segment. Default: True. **TYPE:** `bool` | Example ``` from mixpanel_headless import CohortBreakdown # Segment by saved cohort result = ws.query("Purchase", group_by=CohortBreakdown(123, "Power Users")) # Without "Not In" segment result = ws.query( "Purchase", group_by=CohortBreakdown(123, "Power Users", include_negated=False), ) ``` ### cohort ``` cohort: int | CohortDefinition ``` Saved cohort ID or inline definition. ### name ``` name: str | None = None ``` Display name for the cohort. ### include_negated ``` include_negated: bool = True ``` Whether to include a 'Not In' segment. ### __post_init__ ``` __post_init__() -> None ``` Validate construction arguments. | RAISES | DESCRIPTION | | ------------ | ------------------------------------------------------------------------ | | `ValueError` | If cohort ID is not positive (CB1) or name is empty when provided (CB2). | Source code in `src/mixpanel_headless/types.py` ``` def __post_init__(self) -> None: """Validate construction arguments. Raises: ValueError: If cohort ID is not positive (CB1) or name is empty when provided (CB2). """ _validate_cohort_args(self.cohort, self.name) ``` ## mixpanel_headless.CohortMetric ``` CohortMetric(cohort: int | CohortDefinition, name: str | None = None) ``` Track cohort size over time as an event metric. Represents a cohort size metric for use in the `events=` parameter of `query()` (insights only). Produces a show clause with `behavior.type: "cohort"` in the bookmark JSON. Cannot be used with `query_funnel()`, `query_retention()`, or `query_flow()` (CM4 β€” insights only). Inline `CohortDefinition` is not supported (CM5 β€” server returns 500). Use a saved cohort ID instead. This is enforced at construction. | ATTRIBUTE | DESCRIPTION | | --------- | ------------------------------------------------------------------------------------------------------------ | | `cohort` | Saved cohort ID (positive integer) or inline CohortDefinition. **TYPE:** \`int | | `name` | Display name / series label. Optional for saved cohorts; recommended for inline definitions. **TYPE:** \`str | Example ``` from mixpanel_headless import CohortMetric, Metric, Formula # Track cohort growth result = ws.query(CohortMetric(123, "Power Users"), last=90, unit="week") # Mix with event metrics and formulas result = ws.query( [Metric("Login", math="unique"), CohortMetric(123, "Power Users")], formula="(B / A) * 100", formula_label="Power User %", ) ``` ### cohort ``` cohort: int | CohortDefinition ``` Saved cohort ID or inline definition. ### name ``` name: str | None = None ``` Display name / series label. ### __post_init__ ``` __post_init__() -> None ``` Validate construction arguments. | RAISES | DESCRIPTION | | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------ | | `ValueError` | If cohort ID is not positive (CM1), name is empty when provided (CM2), or cohort is an inline CohortDefinition (CM5 β€” server returns 500). | Source code in `src/mixpanel_headless/types.py` ``` def __post_init__(self) -> None: """Validate construction arguments. Raises: ValueError: If cohort ID is not positive (CM1), name is empty when provided (CM2), or cohort is an inline ``CohortDefinition`` (CM5 β€” server returns 500). """ _validate_cohort_args(self.cohort, self.name) # CM5: Inline CohortDefinition causes server-side 500. if isinstance(self.cohort, CohortDefinition): raise ValueError( "CohortMetric does not support inline CohortDefinition " "(server returns 500). Use a saved cohort ID instead." ) ``` ## Custom Property Query Types Types for using saved or inline custom properties as property references in query breakdowns, filters, and metric measurement. See [Custom Properties in Queries](https://mixpanel.github.io/mixpanel-headless/guide/query/#custom-properties-in-queries) for usage guide. ## mixpanel_headless.CustomPropertyRef ``` CustomPropertyRef(id: int) ``` A reference to a persisted custom property by its integer ID. Used in `GroupBy.property`, `Filter` class methods, and `Metric.property` to reference a custom property that was previously created and saved in Mixpanel. | ATTRIBUTE | DESCRIPTION | | --------- | ---------------------------------------------------------------------------- | | `id` | The custom property's server-assigned ID (must be positive). **TYPE:** `int` | Example ``` from mixpanel_headless import CustomPropertyRef, GroupBy ref = CustomPropertyRef(42) g = GroupBy(property=ref, property_type="number") ``` ### id ``` id: int ``` The custom property's server-assigned ID. ## mixpanel_headless.InlineCustomProperty ``` InlineCustomProperty( formula: str, inputs: dict[str, PropertyInput], property_type: Literal["string", "number", "boolean", "datetime"] | None = None, resource_type: Literal["events", "people"] = "events", ) ``` An ephemeral computed property defined by a formula and input references. Defines a custom property inline at query time without persisting it to Mixpanel. The formula uses variables (A-Z) that map to concrete properties via the `inputs` dict. Can be used in `GroupBy.property`, `Filter` class methods, and `Metric.property` to compute derived values on the fly. | ATTRIBUTE | DESCRIPTION | | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `formula` | Expression in Mixpanel's formula language (max 20,000 chars). **TYPE:** `str` | | `inputs` | Mapping from single uppercase letters (A-Z) to property references. **TYPE:** `dict[str, PropertyInput]` | | `property_type` | Result type of the formula. None defers to the containing type (e.g., GroupBy.property_type). Default: None. **TYPE:** \`Literal['string', 'number', 'boolean', 'datetime'] | | `resource_type` | Data domain β€” "events" or "people". Uses plural form to match Mixpanel's top-level customProperty schema. Default: "events". **TYPE:** `Literal['events', 'people']` | Example ``` from mixpanel_headless import InlineCustomProperty, PropertyInput # Explicit construction icp = InlineCustomProperty( formula="A * B", inputs={ "A": PropertyInput("price", type="number"), "B": PropertyInput("quantity", type="number"), }, property_type="number", ) # Convenience constructor for all-numeric inputs icp = InlineCustomProperty.numeric("A * B", A="price", B="quantity") ``` ### formula ``` formula: str ``` Expression in Mixpanel's formula language. ### inputs ``` inputs: dict[str, PropertyInput] ``` Mapping from single uppercase letters (A-Z) to property references. ### property_type ``` property_type: Literal['string', 'number', 'boolean', 'datetime'] | None = None ``` Result type of the formula; None defers to containing type. ### resource_type ``` resource_type: Literal['events', 'people'] = 'events' ``` Data domain (plural form for top-level customProperty schema). ### numeric ``` numeric(formula: str, /, **properties: str) -> InlineCustomProperty ``` Create an all-numeric-input inline custom property. Convenience constructor that creates `PropertyInput` entries with `type="number"` and `resource_type="event"` for each keyword argument, and sets `property_type="number"`. | PARAMETER | DESCRIPTION | | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | `formula` | Expression in Mixpanel's formula language. **TYPE:** `str` | | `**properties` | Mapping of variable letters to property names. Each key becomes an input key, each value becomes the property name. **TYPE:** `str` **DEFAULT:** `{}` | | RETURNS | DESCRIPTION | | ---------------------- | ------------------------------------------------ | | `InlineCustomProperty` | InlineCustomProperty with all-numeric inputs and | | `InlineCustomProperty` | property_type="number". | Example ``` # Revenue = price * quantity icp = InlineCustomProperty.numeric("A * B", A="price", B="quantity") assert icp.inputs["A"].type == "number" assert icp.property_type == "number" ``` Source code in `src/mixpanel_headless/types.py` ```` @classmethod def numeric( cls, formula: str, /, **properties: str, ) -> InlineCustomProperty: """Create an all-numeric-input inline custom property. Convenience constructor that creates ``PropertyInput`` entries with ``type="number"`` and ``resource_type="event"`` for each keyword argument, and sets ``property_type="number"``. Args: formula: Expression in Mixpanel's formula language. **properties: Mapping of variable letters to property names. Each key becomes an input key, each value becomes the property name. Returns: InlineCustomProperty with all-numeric inputs and ``property_type="number"``. Example: ```python # Revenue = price * quantity icp = InlineCustomProperty.numeric("A * B", A="price", B="quantity") assert icp.inputs["A"].type == "number" assert icp.property_type == "number" ``` """ inputs = { key: PropertyInput(name=value, type="number") for key, value in properties.items() } return cls( formula=formula, inputs=inputs, property_type="number", ) ```` ## mixpanel_headless.PropertyInput ``` PropertyInput( name: str, type: Literal["string", "number", "boolean", "datetime", "list"] = "string", resource_type: Literal["event", "user"] = "event", ) ``` A raw property reference mapping a formula variable to a named property. Used as an entry in :attr:`InlineCustomProperty.inputs` to bind a formula variable (A-Z) to a concrete Mixpanel event or user property. | ATTRIBUTE | DESCRIPTION | | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `name` | The raw property name (e.g., "price", "$browser"). **TYPE:** `str` | | `type` | Property data type. Default: "string". **TYPE:** `Literal['string', 'number', 'boolean', 'datetime', 'list']` | | `resource_type` | Property domain β€” "event" or "user". Uses singular form to match Mixpanel's composedProperties schema. Default: "event". **TYPE:** `Literal['event', 'user']` | Example ``` from mixpanel_headless import PropertyInput pi = PropertyInput("price", type="number") pi_user = PropertyInput("email", resource_type="user") ``` ### name ``` name: str ``` The raw property name. ### type ``` type: Literal['string', 'number', 'boolean', 'datetime', 'list'] = 'string' ``` Property data type. ### resource_type ``` resource_type: Literal['event', 'user'] = 'event' ``` Property domain (singular form for composedProperties schema). ## Advanced Query Types Types for advanced query features β€” period-over-period comparison, frequency analysis, and frequency filtering across query engines. ## mixpanel_headless.TimeComparison ``` TimeComparison( type: TimeComparisonType, unit: TimeComparisonUnit | None = None, date: str | None = None, ) ``` Overlay a comparison time period on insights, funnel, or retention queries. Enables period-over-period analysis by specifying how the comparison window is determined. Three modes are supported: - **relative**: Compare against a prior period offset by `unit` (e.g. previous month, previous week). - **absolute-start**: Compare against a window starting on a fixed date (`date`), running the same duration as the primary range. - **absolute-end**: Compare against a window ending on a fixed date. Use the factory class methods rather than constructing directly: - `TimeComparison.relative(unit)` - `TimeComparison.absolute_start(date)` - `TimeComparison.absolute_end(date)` | ATTRIBUTE | DESCRIPTION | | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | | `type` | Discriminant β€” "relative", "absolute-start", or "absolute-end". **TYPE:** `TimeComparisonType` | | `unit` | Time unit for relative comparison. Required when type="relative", must be None otherwise. **TYPE:** \`TimeComparisonUnit | | `date` | ISO date (YYYY-MM-DD) for absolute comparison. Required when type is "absolute-start" or "absolute-end", must be None otherwise. **TYPE:** \`str | | RAISES | DESCRIPTION | | ------------ | ------------------------------------------------------------- | | `ValueError` | If validation rules TC1-TC3 are violated during construction. | Example ``` from mixpanel_headless.types import TimeComparison # Compare against previous month tc = TimeComparison.relative("month") # Compare against window starting on a fixed date tc = TimeComparison.absolute_start("2026-01-01") # Compare against window ending on a fixed date tc = TimeComparison.absolute_end("2026-12-31") ``` ### type ``` type: TimeComparisonType ``` Discriminant β€” `"relative"`, `"absolute-start"`, or `"absolute-end"`. ### unit ``` unit: TimeComparisonUnit | None = None ``` Time unit for relative comparison (day, week, month, quarter, year). ### date ``` date: str | None = None ``` ISO date (YYYY-MM-DD) for absolute comparison. ### __post_init__ ``` __post_init__() -> None ``` Validate construction arguments (rules TC1-TC3). | RAISES | DESCRIPTION | | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `ValueError` | If type="relative" and unit is None (TC1), or type="relative" and date is set (TC1), or type is absolute and date is None (TC2), or type is absolute and unit is set (TC2), or date does not match YYYY-MM-DD format (TC3). | Source code in `src/mixpanel_headless/types.py` ``` def __post_init__(self) -> None: """Validate construction arguments (rules TC1-TC3). Raises: ValueError: If type="relative" and unit is None (TC1), or type="relative" and date is set (TC1), or type is absolute and date is None (TC2), or type is absolute and unit is set (TC2), or date does not match YYYY-MM-DD format (TC3). """ # TC0: type must be a valid TimeComparisonType valid_types = {"relative", "absolute-start", "absolute-end"} if self.type not in valid_types: raise ValueError( f"TimeComparison type must be one of {sorted(valid_types)}, " f"got {self.type!r}" ) if self.type == "relative": # TC1: relative requires unit, rejects date if self.unit is None: raise ValueError( "TimeComparison type='relative' requires unit to be set " "(e.g., TimeComparison.relative('month'))" ) # TC1b: unit must be a valid TimeComparisonUnit valid_units = {"day", "week", "month", "quarter", "year"} if self.unit not in valid_units: raise ValueError( f"TimeComparison unit must be one of {sorted(valid_units)}, " f"got {self.unit!r}" ) if self.date is not None: raise ValueError( "TimeComparison type='relative' does not accept date; " "use absolute-start or absolute-end for date-based comparison" ) else: # TC2: absolute-start / absolute-end requires date, rejects unit if self.date is None: raise ValueError( f"TimeComparison type={self.type!r} requires date to be set " f"(e.g., TimeComparison.absolute_start('2026-01-01'))" ) if self.unit is not None: raise ValueError( f"TimeComparison type={self.type!r} does not accept unit; " f"unit is only valid for type='relative'" ) # TC3: date must be a valid YYYY-MM-DD calendar date if not _DATE_RE.match(self.date): raise ValueError( f"TimeComparison date must be in YYYY-MM-DD format, " f"got {self.date!r}" ) # TC3b: verify it's a real calendar date (e.g. reject 2026-02-30) try: import datetime datetime.date.fromisoformat(self.date) except ValueError: raise ValueError( f"TimeComparison date is not a valid calendar date: {self.date!r}" ) from None ``` ### relative ``` relative(unit: TimeComparisonUnit) -> TimeComparison ``` Create a relative time comparison. Compares against a prior period offset by the given unit (e.g. previous month, previous week). | PARAMETER | DESCRIPTION | | --------- | --------------------------------------------------------------------------------------------------------------------- | | `unit` | Time unit for the comparison offset. One of "day", "week", "month", "quarter", "year". **TYPE:** `TimeComparisonUnit` | | RETURNS | DESCRIPTION | | ---------------- | --------------------------------------------- | | `TimeComparison` | A TimeComparison with type="relative" and the | | `TimeComparison` | specified unit. | Example ``` tc = TimeComparison.relative("month") # type="relative", unit="month", date=None ``` Source code in `src/mixpanel_headless/types.py` ```` @classmethod def relative(cls, unit: TimeComparisonUnit) -> TimeComparison: """Create a relative time comparison. Compares against a prior period offset by the given unit (e.g. previous month, previous week). Args: unit: Time unit for the comparison offset. One of ``"day"``, ``"week"``, ``"month"``, ``"quarter"``, ``"year"``. Returns: A ``TimeComparison`` with ``type="relative"`` and the specified ``unit``. Example: ```python tc = TimeComparison.relative("month") # type="relative", unit="month", date=None ``` """ return cls(type="relative", unit=unit) ```` ### absolute_start ``` absolute_start(date: str) -> TimeComparison ``` Create an absolute-start time comparison. Compares against a window that starts on the given date, running the same duration as the primary query range. | PARAMETER | DESCRIPTION | | --------- | ------------------------------------------------ | | `date` | Start date in YYYY-MM-DD format. **TYPE:** `str` | | RETURNS | DESCRIPTION | | ---------------- | ----------------------------------------------- | | `TimeComparison` | A TimeComparison with type="absolute-start" and | | `TimeComparison` | the specified date. | | RAISES | DESCRIPTION | | ------------ | ------------------------------------------ | | `ValueError` | If date is not in YYYY-MM-DD format (TC3). | Example ``` tc = TimeComparison.absolute_start("2026-01-01") # type="absolute-start", unit=None, date="2026-01-01" ``` Source code in `src/mixpanel_headless/types.py` ```` @classmethod def absolute_start(cls, date: str) -> TimeComparison: """Create an absolute-start time comparison. Compares against a window that starts on the given date, running the same duration as the primary query range. Args: date: Start date in YYYY-MM-DD format. Returns: A ``TimeComparison`` with ``type="absolute-start"`` and the specified ``date``. Raises: ValueError: If date is not in YYYY-MM-DD format (TC3). Example: ```python tc = TimeComparison.absolute_start("2026-01-01") # type="absolute-start", unit=None, date="2026-01-01" ``` """ return cls(type="absolute-start", date=date) ```` ### absolute_end ``` absolute_end(date: str) -> TimeComparison ``` Create an absolute-end time comparison. Compares against a window that ends on the given date, running the same duration as the primary query range. | PARAMETER | DESCRIPTION | | --------- | ---------------------------------------------- | | `date` | End date in YYYY-MM-DD format. **TYPE:** `str` | | RETURNS | DESCRIPTION | | ---------------- | --------------------------------------------- | | `TimeComparison` | A TimeComparison with type="absolute-end" and | | `TimeComparison` | the specified date. | | RAISES | DESCRIPTION | | ------------ | ------------------------------------------ | | `ValueError` | If date is not in YYYY-MM-DD format (TC3). | Example ``` tc = TimeComparison.absolute_end("2026-12-31") # type="absolute-end", unit=None, date="2026-12-31" ``` Source code in `src/mixpanel_headless/types.py` ```` @classmethod def absolute_end(cls, date: str) -> TimeComparison: """Create an absolute-end time comparison. Compares against a window that ends on the given date, running the same duration as the primary query range. Args: date: End date in YYYY-MM-DD format. Returns: A ``TimeComparison`` with ``type="absolute-end"`` and the specified ``date``. Raises: ValueError: If date is not in YYYY-MM-DD format (TC3). Example: ```python tc = TimeComparison.absolute_end("2026-12-31") # type="absolute-end", unit=None, date="2026-12-31" ``` """ return cls(type="absolute-end", date=date) ```` ## mixpanel_headless.FrequencyBreakdown ``` FrequencyBreakdown( event: str, bucket_size: int = 1, bucket_min: int = 0, bucket_max: int = 10, label: str | None = None, ) ``` Break down query results by how often users performed an event. Used with `Workspace.query()` / `build_params()` in the `group_by=` parameter to segment users by event frequency. | ATTRIBUTE | DESCRIPTION | | ------------- | ----------------------------------------------------------------------------------------------------------------- | | `event` | Event name to count frequency for. **TYPE:** `str` | | `bucket_size` | Width of each frequency bucket. Default: 1. **TYPE:** `int` | | `bucket_min` | Minimum frequency value. Default: 0. **TYPE:** `int` | | `bucket_max` | Maximum frequency value. Default: 10. **TYPE:** `int` | | `label` | Display label for the breakdown. None generates " Frequency" (e.g., "Purchase Frequency"). **TYPE:** \`str | | RAISES | DESCRIPTION | | ------------ | ----------------------------------------- | | `ValueError` | If validation rules FB1-FB4 are violated. | Example ``` from mixpanel_headless import FrequencyBreakdown # How often users purchased (0-10 in increments of 1) result = ws.query("Login", group_by=FrequencyBreakdown("Purchase")) # Custom buckets: 0-50 in increments of 5 result = ws.query( "Login", group_by=FrequencyBreakdown( "Purchase", bucket_size=5, bucket_min=0, bucket_max=50 ), ) ``` ### event ``` event: str ``` Event name to count frequency for. ### bucket_size ``` bucket_size: int = 1 ``` Width of each frequency bucket. ### bucket_min ``` bucket_min: int = 0 ``` Minimum frequency value. ### bucket_max ``` bucket_max: int = 10 ``` Maximum frequency value. ### label ``` label: str | None = None ``` Display label for the breakdown. ### __post_init__ ``` __post_init__() -> None ``` Validate construction arguments (rules FB1-FB4). | RAISES | DESCRIPTION | | ------------ | ---------------------------------------------------------------------------------------------------------------------------- | | `ValueError` | If event is empty (FB1), bucket_size is not positive (FB2), bucket_min >= bucket_max (FB3), or bucket_min is negative (FB4). | Source code in `src/mixpanel_headless/types.py` ``` def __post_init__(self) -> None: """Validate construction arguments (rules FB1-FB4). Raises: ValueError: If event is empty (FB1), bucket_size is not positive (FB2), bucket_min >= bucket_max (FB3), or bucket_min is negative (FB4). """ # FB1: event must be non-empty if not self.event.strip(): raise ValueError("FrequencyBreakdown.event must be a non-empty string") # FB2: bucket_size must be positive if self.bucket_size <= 0: raise ValueError( f"FrequencyBreakdown.bucket_size must be positive, " f"got {self.bucket_size}" ) # FB4: bucket_min must be non-negative (check before FB3 for clarity) if self.bucket_min < 0: raise ValueError( f"FrequencyBreakdown.bucket_min must be non-negative, " f"got {self.bucket_min}" ) # FB3: bucket_min must be < bucket_max if self.bucket_min >= self.bucket_max: raise ValueError( f"FrequencyBreakdown.bucket_min ({self.bucket_min}) must be " f"less than bucket_max ({self.bucket_max})" ) ``` ## mixpanel_headless.FrequencyFilter ``` FrequencyFilter( event: str, value: int | float, operator: FrequencyFilterOperator = "is at least", date_range_value: int | None = None, date_range_unit: Literal["day", "week", "month"] | None = None, event_filters: list[Filter] | None = None, label: str | None = None, ) ``` Filter query results by how often users performed an event. Used with `Workspace.query()` / `build_params()` in the `where=` parameter to restrict results to users meeting a frequency threshold. | ATTRIBUTE | DESCRIPTION | | ------------------ | -------------------------------------------------------------------------------------------------------------------------------- | | `event` | Event name to count frequency for. **TYPE:** `str` | | `operator` | Comparison operator. Default: "is at least". **TYPE:** `FrequencyFilterOperator` | | `value` | Threshold value for the comparison. **TYPE:** \`int | | `date_range_value` | Lookback window size. Must be paired with date_range_unit. **TYPE:** \`int | | `date_range_unit` | Lookback window unit ("day", "week", "month"). Must be paired with date_range_value. **TYPE:** \`Literal['day', 'week', 'month'] | | `event_filters` | Property filters applied to the frequency event before counting. **TYPE:** \`list[Filter] | | `label` | Display label for the filter. **TYPE:** \`str | | RAISES | DESCRIPTION | | ------------ | ----------------------------------------- | | `ValueError` | If validation rules FF1-FF5 are violated. | Example ``` from mixpanel_headless import FrequencyFilter # Users who logged in at least 5 times result = ws.query("Purchase", where=FrequencyFilter("Login", value=5)) # Users who purchased 3+ times in the last 30 days result = ws.query( "Login", where=FrequencyFilter( "Purchase", value=3, date_range_value=30, date_range_unit="day", ), ) ``` ### event ``` event: str ``` Event name to count frequency for. ### value ``` value: int | float ``` Threshold value for the comparison. ### operator ``` operator: FrequencyFilterOperator = 'is at least' ``` Comparison operator. ### date_range_value ``` date_range_value: int | None = None ``` Lookback window size. ### date_range_unit ``` date_range_unit: Literal['day', 'week', 'month'] | None = None ``` Lookback window unit. ### event_filters ``` event_filters: list[Filter] | None = None ``` Property filters applied to the frequency event. ### label ``` label: str | None = None ``` Display label for the filter. ### __post_init__ ``` __post_init__() -> None ``` Validate construction arguments (rules FF1-FF5). | RAISES | DESCRIPTION | | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `ValueError` | If event is empty (FF1), operator is invalid (FF2), value is negative (FF3), date_range_value and date_range_unit are not both set or both None (FF4), or date_range_value is not positive when set (FF5). | Source code in `src/mixpanel_headless/types.py` ``` def __post_init__(self) -> None: """Validate construction arguments (rules FF1-FF5). Raises: ValueError: If event is empty (FF1), operator is invalid (FF2), value is negative (FF3), date_range_value and date_range_unit are not both set or both None (FF4), or date_range_value is not positive when set (FF5). """ from mixpanel_headless._internal.bookmark_enums import ( VALID_FREQUENCY_FILTER_OPERATORS, ) # FF1: event must be non-empty if not self.event.strip(): raise ValueError("FrequencyFilter.event must be a non-empty string") # FF2: operator must be valid if self.operator not in VALID_FREQUENCY_FILTER_OPERATORS: valid = ", ".join(sorted(VALID_FREQUENCY_FILTER_OPERATORS)) raise ValueError( f"FrequencyFilter.operator must be one of: {valid}; " f"got {self.operator!r}" ) # FF3: value must be non-negative if self.value < 0: raise ValueError( f"FrequencyFilter.value must be non-negative, got {self.value}" ) # FF4: date_range_value and date_range_unit must both be set or both None has_value = self.date_range_value is not None has_unit = self.date_range_unit is not None if has_value != has_unit: raise ValueError( "FrequencyFilter.date_range_value and date_range_unit must " "both be set or both be None; got date_range_value=" f"{self.date_range_value!r}, date_range_unit=" f"{self.date_range_unit!r}" ) # FF5: date_range_value must be positive if set if self.date_range_value is not None and self.date_range_value <= 0: raise ValueError( f"FrequencyFilter.date_range_value must be positive when set, " f"got {self.date_range_value}" ) ``` ## Cohort Definition Types Types for building inline cohort definitions programmatically β€” used with `Filter.in_cohort()`, `CohortBreakdown`, and `CohortMetric`. ## mixpanel_headless.CohortDefinition ``` CohortDefinition(*criteria: CohortCriteria | CohortDefinition) ``` A composed set of criteria combined with AND/OR logic. Produces valid Mixpanel cohort definition JSON (legacy `selector` + `behaviors` format) via `to_dict()`. Behavior keys are globally re-indexed to ensure uniqueness across arbitrary nesting. Example ``` from mixpanel_headless import CohortCriteria, CohortDefinition cohort = CohortDefinition.all_of( CohortCriteria.has_property("plan", "premium"), CohortCriteria.did_event("Purchase", at_least=3, within_days=30), ) result = cohort.to_dict() # {"selector": {...}, "behaviors": {"bhvr_0": {...}}} ``` Create a definition combining criteria with AND logic. Equivalent to `CohortDefinition.all_of(*criteria)`. | PARAMETER | DESCRIPTION | | ----------- | ---------------------------------------------------------------------- | | `*criteria` | One or more criteria or nested definitions. **TYPE:** \`CohortCriteria | | RAISES | DESCRIPTION | | ------------ | ---------------------------------- | | `ValueError` | If no criteria are provided (CD9). | Source code in `src/mixpanel_headless/types.py` ``` def __init__( self, *criteria: CohortCriteria | CohortDefinition, ) -> None: """Create a definition combining criteria with AND logic. Equivalent to ``CohortDefinition.all_of(*criteria)``. Args: *criteria: One or more criteria or nested definitions. Raises: ValueError: If no criteria are provided (CD9). """ if not criteria: raise ValueError("CohortDefinition requires at least one criterion") object.__setattr__(self, "_criteria", criteria) object.__setattr__(self, "_operator", "and") ``` ### all_of ``` all_of(*criteria: CohortCriteria | CohortDefinition) -> CohortDefinition ``` Combine criteria and/or definitions with AND logic. | PARAMETER | DESCRIPTION | | ----------- | ---------------------------------------------------------------------- | | `*criteria` | One or more criteria or nested definitions. **TYPE:** \`CohortCriteria | | RETURNS | DESCRIPTION | | ------------------ | ------------------------------------- | | `CohortDefinition` | CohortDefinition with AND combinator. | | RAISES | DESCRIPTION | | ------------ | ---------------------------------- | | `ValueError` | If no criteria are provided (CD9). | Source code in `src/mixpanel_headless/types.py` ``` @classmethod def all_of( cls, *criteria: CohortCriteria | CohortDefinition, ) -> CohortDefinition: """Combine criteria and/or definitions with AND logic. Args: *criteria: One or more criteria or nested definitions. Returns: CohortDefinition with AND combinator. Raises: ValueError: If no criteria are provided (CD9). """ if not criteria: raise ValueError("CohortDefinition requires at least one criterion") instance = cls.__new__(cls) object.__setattr__(instance, "_criteria", criteria) object.__setattr__(instance, "_operator", "and") return instance ``` ### any_of ``` any_of(*criteria: CohortCriteria | CohortDefinition) -> CohortDefinition ``` Combine criteria and/or definitions with OR logic. | PARAMETER | DESCRIPTION | | ----------- | ---------------------------------------------------------------------- | | `*criteria` | One or more criteria or nested definitions. **TYPE:** \`CohortCriteria | | RETURNS | DESCRIPTION | | ------------------ | ------------------------------------ | | `CohortDefinition` | CohortDefinition with OR combinator. | | RAISES | DESCRIPTION | | ------------ | ---------------------------------- | | `ValueError` | If no criteria are provided (CD9). | Source code in `src/mixpanel_headless/types.py` ``` @classmethod def any_of( cls, *criteria: CohortCriteria | CohortDefinition, ) -> CohortDefinition: """Combine criteria and/or definitions with OR logic. Args: *criteria: One or more criteria or nested definitions. Returns: CohortDefinition with OR combinator. Raises: ValueError: If no criteria are provided (CD9). """ if not criteria: raise ValueError("CohortDefinition requires at least one criterion") instance = cls.__new__(cls) object.__setattr__(instance, "_criteria", criteria) object.__setattr__(instance, "_operator", "or") return instance ``` ### to_dict ``` to_dict() -> dict[str, Any] ``` Serialize to Mixpanel cohort definition format. Produces `{"selector": {...}, "behaviors": {...}}` with globally re-indexed behavior keys (`bhvr_0`, `bhvr_1`, ...) ensuring uniqueness across arbitrary nesting depth. | RETURNS | DESCRIPTION | | ---------------- | ----------------------------------------------------- | | `dict[str, Any]` | Dict with selector expression tree and behaviors map. | Example ``` cohort = CohortDefinition.all_of( CohortCriteria.has_property("plan", "premium"), CohortCriteria.did_event("Purchase", at_least=3, within_days=30), ) data = cohort.to_dict() # {"selector": {"operator": "and", "children": [...]}, # "behaviors": {"bhvr_0": {...}}} # Pass directly to cohort CRUD: ws.create_cohort(CreateCohortParams( name="Premium Purchasers", definition=data, )) ``` Source code in `src/mixpanel_headless/types.py` ```` def to_dict(self) -> dict[str, Any]: """Serialize to Mixpanel cohort definition format. Produces ``{"selector": {...}, "behaviors": {...}}`` with globally re-indexed behavior keys (``bhvr_0``, ``bhvr_1``, ...) ensuring uniqueness across arbitrary nesting depth. Returns: Dict with ``selector`` expression tree and ``behaviors`` map. Example: ```python cohort = CohortDefinition.all_of( CohortCriteria.has_property("plan", "premium"), CohortCriteria.did_event("Purchase", at_least=3, within_days=30), ) data = cohort.to_dict() # {"selector": {"operator": "and", "children": [...]}, # "behaviors": {"bhvr_0": {...}}} # Pass directly to cohort CRUD: ws.create_cohort(CreateCohortParams( name="Premium Purchasers", definition=data, )) ``` """ # CD10: Behavior key uniqueness is enforced by sequential re-indexing # (bhvr_0, bhvr_1, ...) during tree traversal below. behaviors: dict[str, Any] = {} counter = [0] # mutable container for closure def _collect_and_build( item: CohortCriteria | CohortDefinition, ) -> dict[str, Any]: """Recursively build selector tree and collect behaviors. Args: item: Criterion or nested definition to process. Returns: Selector node dict (leaf or combinator). """ if isinstance(item, CohortCriteria): # Deep copy: operand may be a mutable list (e.g. has_property # with list value), so shallow dict() is not sufficient. node = copy.deepcopy(item._selector_node) if item._behavior_key is not None and item._behavior is not None: new_key = f"bhvr_{counter[0]}" counter[0] += 1 behaviors[new_key] = copy.deepcopy(item._behavior) node["value"] = new_key return node # CohortDefinition: recurse into children children = [_collect_and_build(c) for c in item._criteria] return { "operator": item._operator, "children": children, } selector = _collect_and_build(self) return {"selector": selector, "behaviors": behaviors} ```` ## mixpanel_headless.CohortCriteria ``` CohortCriteria( _selector_node: dict[str, Any], _behavior_key: str | None, _behavior: dict[str, Any] | None, ) ``` A single atomic condition for cohort membership. Constructed exclusively via class methods β€” never instantiate directly. Produces selector nodes and behavior entries for the Mixpanel cohort definition format (legacy `selector` + `behaviors` JSON). Example ``` from mixpanel_headless import CohortCriteria # Behavioral criterion c = CohortCriteria.did_event("Purchase", at_least=3, within_days=30) # Property criterion c = CohortCriteria.has_property("plan", "premium") # Cohort reference c = CohortCriteria.in_cohort(456) ``` ### did_event ``` did_event( event: str, *, at_least: int | None = None, at_most: int | None = None, exactly: int | None = None, within_days: int | None = None, within_weeks: int | None = None, within_months: int | None = None, from_date: str | None = None, to_date: str | None = None, where: Filter | list[Filter] | None = None, aggregation: CohortAggregationType | None = None, aggregation_property: str | None = None, ) -> CohortCriteria ``` Create a behavioral criterion based on event frequency. | PARAMETER | DESCRIPTION | | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `event` | Event name (must be non-empty). **TYPE:** `str` | | `at_least` | Minimum event count (>=). **TYPE:** \`int | | `at_most` | Maximum event count (\<=). **TYPE:** \`int | | `exactly` | Exact event count (==). **TYPE:** \`int | | `within_days` | Rolling window in days. **TYPE:** \`int | | `within_weeks` | Rolling window in weeks. **TYPE:** \`int | | `within_months` | Rolling window in months. **TYPE:** \`int | | `from_date` | Absolute start date (YYYY-MM-DD). **TYPE:** \`str | | `to_date` | Absolute end date (YYYY-MM-DD). **TYPE:** \`str | | `where` | Event property filter(s). **TYPE:** \`Filter | | `aggregation` | Aggregation operator for property-based thresholds (total, unique, average, min, max, median). Must be paired with aggregation_property. **TYPE:** \`CohortAggregationType | | `aggregation_property` | Event property to aggregate (e.g., "amount"). Must be paired with aggregation. **TYPE:** \`str | | RETURNS | DESCRIPTION | | ---------------- | ---------------------------------------------------------------- | | `CohortCriteria` | CohortCriteria with behavioral selector node and behavior entry. | | RAISES | DESCRIPTION | | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `ValueError` | If no frequency param or multiple are set, frequency is negative, event name is empty/whitespace, time constraints are missing or conflicting, dates are malformed/misordered, or aggregation and aggregation_property are not both set or both None (CA1/CA2). | Source code in `src/mixpanel_headless/types.py` ``` @classmethod def did_event( cls, event: str, *, at_least: int | None = None, at_most: int | None = None, exactly: int | None = None, within_days: int | None = None, within_weeks: int | None = None, within_months: int | None = None, from_date: str | None = None, to_date: str | None = None, where: Filter | list[Filter] | None = None, aggregation: CohortAggregationType | None = None, aggregation_property: str | None = None, ) -> CohortCriteria: """Create a behavioral criterion based on event frequency. Args: event: Event name (must be non-empty). at_least: Minimum event count (``>=``). at_most: Maximum event count (``<=``). exactly: Exact event count (``==``). within_days: Rolling window in days. within_weeks: Rolling window in weeks. within_months: Rolling window in months. from_date: Absolute start date (YYYY-MM-DD). to_date: Absolute end date (YYYY-MM-DD). where: Event property filter(s). aggregation: Aggregation operator for property-based thresholds (total, unique, average, min, max, median). Must be paired with ``aggregation_property``. aggregation_property: Event property to aggregate (e.g., ``"amount"``). Must be paired with ``aggregation``. Returns: CohortCriteria with behavioral selector node and behavior entry. Raises: ValueError: If no frequency param or multiple are set, frequency is negative, event name is empty/whitespace, time constraints are missing or conflicting, dates are malformed/misordered, or aggregation and aggregation_property are not both set or both None (CA1/CA2). """ # CD4: Event name must be non-empty if not event or not event.strip(): raise ValueError("event name must be non-empty") # CA1/CA2: aggregation and aggregation_property must both be set or both None if (aggregation is None) != (aggregation_property is None): raise ValueError( "aggregation and aggregation_property must both be set or both be None" ) if aggregation_property is not None and not aggregation_property.strip(): raise ValueError("aggregation_property must be a non-empty string") # CD1: Exactly one frequency param required freq_params = { "at_least": at_least, "at_most": at_most, "exactly": exactly, } set_freqs = {k: v for k, v in freq_params.items() if v is not None} if len(set_freqs) != 1: raise ValueError("exactly one of at_least, at_most, exactly must be set") freq_name, freq_value = next(iter(set_freqs.items())) # CD2: Frequency param must be non-negative if freq_value < 0: raise ValueError("frequency value must be >= 0") # Map frequency param to selector operator freq_operator_map = { "at_least": ">=", "at_most": "<=", "exactly": "==", } selector_operator = freq_operator_map[freq_name] # CD3: Exactly one time constraint required rolling_params = { "within_days": within_days, "within_weeks": within_weeks, "within_months": within_months, } set_rolling = {k: v for k, v in rolling_params.items() if v is not None} has_date_range = from_date is not None or to_date is not None if not set_rolling and not has_date_range: raise ValueError( "exactly one time constraint required " "(within_days/weeks/months or from_date+to_date)" ) if set_rolling and has_date_range: raise ValueError( "exactly one time constraint required " "(within_days/weeks/months or from_date+to_date)" ) if len(set_rolling) > 1: raise ValueError( "exactly one time constraint required " "(within_days/weeks/months or from_date+to_date)" ) # Build behavior entry behavior_key = "bhvr_0" # placeholder, re-indexed by to_dict() event_selector: dict[str, Any] = { "event": event, "selector": None, } if where is not None: where_list = [where] if isinstance(where, Filter) else where if where_list: event_selector["selector"] = _build_event_selector(where_list) count_dict: dict[str, Any] = { "event_selector": event_selector, "type": "absolute", } # Add aggregation fields when set if aggregation is not None and aggregation_property is not None: count_dict["aggregationOperator"] = aggregation count_dict["property"] = aggregation_property behavior: dict[str, Any] = { "count": count_dict, } if set_rolling: rolling_name, rolling_value = next(iter(set_rolling.items())) if rolling_value <= 0: raise ValueError("time window value must be positive") unit_map = { "within_days": "day", "within_weeks": "week", "within_months": "month", } behavior["window"] = { "unit": unit_map[rolling_name], "value": rolling_value, } else: # Absolute date range # CD5: from_date requires to_date (and vice versa) if from_date is not None and to_date is None: raise ValueError("from_date requires to_date") if to_date is not None and from_date is None: raise ValueError("to_date requires from_date") # CD6: Dates must be YYYY-MM-DD # from_date and to_date are guaranteed non-None here: # has_date_range is True and CD5 guards above reject mismatched pairs. if from_date is None or to_date is None: # pragma: no cover raise ValueError( "exactly one time constraint required " "(within_days/weeks/months or from_date+to_date)" ) _validate_cohort_date(from_date) _validate_cohort_date(to_date) if dt_date.fromisoformat(from_date) > dt_date.fromisoformat(to_date): raise ValueError("from_date must be before or equal to to_date") behavior["from_date"] = from_date behavior["to_date"] = to_date selector_node: dict[str, Any] = { "property": "behaviors", "value": behavior_key, "operator": selector_operator, "operand": freq_value, } return cls( _selector_node=selector_node, _behavior_key=behavior_key, _behavior=behavior, ) ``` ### did_not_do_event ``` did_not_do_event( event: str, *, within_days: int | None = None, within_weeks: int | None = None, within_months: int | None = None, from_date: str | None = None, to_date: str | None = None, ) -> CohortCriteria ``` Create a criterion for users who did NOT perform an event. Shorthand for `did_event(event, exactly=0, ...)`. | PARAMETER | DESCRIPTION | | --------------- | ------------------------------------------------- | | `event` | Event name. **TYPE:** `str` | | `within_days` | Rolling window in days. **TYPE:** \`int | | `within_weeks` | Rolling window in weeks. **TYPE:** \`int | | `within_months` | Rolling window in months. **TYPE:** \`int | | `from_date` | Absolute start date (YYYY-MM-DD). **TYPE:** \`str | | `to_date` | Absolute end date (YYYY-MM-DD). **TYPE:** \`str | | RETURNS | DESCRIPTION | | ---------------- | -------------------------------------------------------------- | | `CohortCriteria` | CohortCriteria equivalent to did_event(event, exactly=0, ...). | | RAISES | DESCRIPTION | | ------------ | ------------------------- | | `ValueError` | On constraint violations. | Source code in `src/mixpanel_headless/types.py` ``` @classmethod def did_not_do_event( cls, event: str, *, within_days: int | None = None, within_weeks: int | None = None, within_months: int | None = None, from_date: str | None = None, to_date: str | None = None, ) -> CohortCriteria: """Create a criterion for users who did NOT perform an event. Shorthand for ``did_event(event, exactly=0, ...)``. Args: event: Event name. within_days: Rolling window in days. within_weeks: Rolling window in weeks. within_months: Rolling window in months. from_date: Absolute start date (YYYY-MM-DD). to_date: Absolute end date (YYYY-MM-DD). Returns: CohortCriteria equivalent to ``did_event(event, exactly=0, ...)``. Raises: ValueError: On constraint violations. """ return cls.did_event( event, exactly=0, within_days=within_days, within_weeks=within_weeks, within_months=within_months, from_date=from_date, to_date=to_date, ) ``` ### has_property ``` has_property( property: str, value: str | int | float | bool | list[str], *, operator: Literal[ "equals", "not_equals", "contains", "not_contains", "greater_than", "less_than", "is_set", "is_not_set", ] = "equals", property_type: Literal[ "string", "number", "boolean", "datetime", "list" ] = "string", ) -> CohortCriteria ``` Create a property-based criterion. | PARAMETER | DESCRIPTION | | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `property` | Property name (must be non-empty). **TYPE:** `str` | | `value` | Value to compare against. **TYPE:** \`str | | `operator` | Comparison operator. Default: "equals". **TYPE:** `Literal['equals', 'not_equals', 'contains', 'not_contains', 'greater_than', 'less_than', 'is_set', 'is_not_set']` **DEFAULT:** `'equals'` | | `property_type` | Data type of the property. Default: "string". **TYPE:** `Literal['string', 'number', 'boolean', 'datetime', 'list']` **DEFAULT:** `'string'` | | RETURNS | DESCRIPTION | | ---------------- | ------------------------------------------- | | `CohortCriteria` | CohortCriteria with property selector node. | | RAISES | DESCRIPTION | | ------------ | -------------------------------- | | `ValueError` | If property name is empty (CD7). | Source code in `src/mixpanel_headless/types.py` ``` @classmethod def has_property( cls, property: str, value: str | int | float | bool | list[str], *, operator: Literal[ "equals", "not_equals", "contains", "not_contains", "greater_than", "less_than", "is_set", "is_not_set", ] = "equals", property_type: Literal[ "string", "number", "boolean", "datetime", "list", ] = "string", ) -> CohortCriteria: """Create a property-based criterion. Args: property: Property name (must be non-empty). value: Value to compare against. operator: Comparison operator. Default: ``"equals"``. property_type: Data type of the property. Default: ``"string"``. Returns: CohortCriteria with property selector node. Raises: ValueError: If property name is empty (CD7). """ # CD7: Property name must be non-empty if not property or not property.strip(): raise ValueError("property name must be non-empty") selector_operator = _PROPERTY_OPERATOR_MAP[operator] selector_node: dict[str, Any] = { "property": "user", "value": property, "operator": selector_operator, "operand": value, "type": property_type, } return cls( _selector_node=selector_node, _behavior_key=None, _behavior=None, ) ``` ### property_is_set ``` property_is_set(property: str) -> CohortCriteria ``` Check if a user property exists. Shorthand for `has_property(property, "", operator="is_set")`. | PARAMETER | DESCRIPTION | | ---------- | ------------------------------ | | `property` | Property name. **TYPE:** `str` | | RETURNS | DESCRIPTION | | ---------------- | ------------------------------------------- | | `CohortCriteria` | CohortCriteria checking property existence. | | RAISES | DESCRIPTION | | ------------ | -------------------------------- | | `ValueError` | If property name is empty (CD7). | Source code in `src/mixpanel_headless/types.py` ``` @classmethod def property_is_set(cls, property: str) -> CohortCriteria: """Check if a user property exists. Shorthand for ``has_property(property, "", operator="is_set")``. Args: property: Property name. Returns: CohortCriteria checking property existence. Raises: ValueError: If property name is empty (CD7). """ return cls.has_property(property, "", operator="is_set") ``` ### property_is_not_set ``` property_is_not_set(property: str) -> CohortCriteria ``` Check if a user property does not exist. Shorthand for `has_property(property, "", operator="is_not_set")`. | PARAMETER | DESCRIPTION | | ---------- | ------------------------------ | | `property` | Property name. **TYPE:** `str` | | RETURNS | DESCRIPTION | | ---------------- | ----------------------------------------------- | | `CohortCriteria` | CohortCriteria checking property non-existence. | | RAISES | DESCRIPTION | | ------------ | -------------------------------- | | `ValueError` | If property name is empty (CD7). | Source code in `src/mixpanel_headless/types.py` ``` @classmethod def property_is_not_set(cls, property: str) -> CohortCriteria: """Check if a user property does not exist. Shorthand for ``has_property(property, "", operator="is_not_set")``. Args: property: Property name. Returns: CohortCriteria checking property non-existence. Raises: ValueError: If property name is empty (CD7). """ return cls.has_property(property, "", operator="is_not_set") ``` ### in_cohort ``` in_cohort(cohort_id: int) -> CohortCriteria ``` Create a criterion for membership in a saved cohort. | PARAMETER | DESCRIPTION | | ----------- | ----------------------------------------------------- | | `cohort_id` | Cohort ID (must be positive integer). **TYPE:** `int` | | RETURNS | DESCRIPTION | | ---------------- | --------------------------------------------------- | | `CohortCriteria` | CohortCriteria with cohort reference selector node. | | RAISES | DESCRIPTION | | ------------ | --------------------------------------------- | | `ValueError` | If cohort_id is not a positive integer (CD8). | Source code in `src/mixpanel_headless/types.py` ``` @classmethod def in_cohort(cls, cohort_id: int) -> CohortCriteria: """Create a criterion for membership in a saved cohort. Args: cohort_id: Cohort ID (must be positive integer). Returns: CohortCriteria with cohort reference selector node. Raises: ValueError: If cohort_id is not a positive integer (CD8). """ if cohort_id <= 0: raise ValueError("cohort_id must be a positive integer") selector_node: dict[str, Any] = { "property": "cohort", "value": cohort_id, "operator": "in", } return cls( _selector_node=selector_node, _behavior_key=None, _behavior=None, ) ``` ### not_in_cohort ``` not_in_cohort(cohort_id: int) -> CohortCriteria ``` Create a criterion for non-membership in a saved cohort. | PARAMETER | DESCRIPTION | | ----------- | ----------------------------------------------------- | | `cohort_id` | Cohort ID (must be positive integer). **TYPE:** `int` | | RETURNS | DESCRIPTION | | ---------------- | --------------------------------------------------- | | `CohortCriteria` | CohortCriteria with cohort exclusion selector node. | | RAISES | DESCRIPTION | | ------------ | --------------------------------------------- | | `ValueError` | If cohort_id is not a positive integer (CD8). | Source code in `src/mixpanel_headless/types.py` ``` @classmethod def not_in_cohort(cls, cohort_id: int) -> CohortCriteria: """Create a criterion for non-membership in a saved cohort. Args: cohort_id: Cohort ID (must be positive integer). Returns: CohortCriteria with cohort exclusion selector node. Raises: ValueError: If cohort_id is not a positive integer (CD8). """ if cohort_id <= 0: raise ValueError("cohort_id must be a positive integer") selector_node: dict[str, Any] = { "property": "cohort", "value": cohort_id, "operator": "not in", } return cls( _selector_node=selector_node, _behavior_key=None, _behavior=None, ) ``` ## Funnel Query Types Types for `Workspace.query_funnel()` β€” typed funnel conversion analysis with step definitions, exclusions, and conversion windows. ## mixpanel_headless.FunnelStep ``` FunnelStep( event: str, label: str | None = None, filters: list[Filter] | None = None, filters_combinator: FiltersCombinator = "all", order: FunnelOrder | None = None, ) ``` A single step in a funnel query. Use plain event-name strings for simple funnels. Use `FunnelStep` objects when you need per-step filters, labels, or ordering overrides. | ATTRIBUTE | DESCRIPTION | | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | | `event` | Mixpanel event name for this funnel step. **TYPE:** `str` | | `label` | Display label for this step. Defaults to the event name when None. **TYPE:** \`str | | `filters` | Per-step filter conditions. Each Filter restricts which events count for this step. None means no filters. **TYPE:** \`list[Filter] | | `filters_combinator` | How per-step filters combine. "all" requires all filters to match (AND logic). "any" requires any filter to match (OR logic). **TYPE:** `FiltersCombinator` | | `order` | Per-step ordering override. Only meaningful when the top-level funnel order is "any". None inherits the top-level order. **TYPE:** \`FunnelOrder | Example ``` from mixpanel_headless import FunnelStep, Filter # Simple step (equivalent to just using "Signup" string) step1 = FunnelStep("Signup") # Step with per-step filter and label step2 = FunnelStep( "Purchase", label="High-Value Purchase", filters=[Filter.greater_than("amount", 50)], ) ws.query_funnel([step1, step2]) ``` ### event ``` event: str ``` Mixpanel event name for this funnel step. ### label ``` label: str | None = None ``` Display label for this step (defaults to event name). ### filters ``` filters: list[Filter] | None = None ``` Per-step filter conditions. ### filters_combinator ``` filters_combinator: FiltersCombinator = 'all' ``` How per-step filters combine (AND/OR). ### order ``` order: FunnelOrder | None = None ``` Per-step ordering override (only meaningful with top-level order='any'). ### __post_init__ ``` __post_init__() -> None ``` Validate construction arguments. | RAISES | DESCRIPTION | | ------------ | ------------------------------------------------------- | | `ValueError` | If event is empty or contains control characters (FS1). | Source code in `src/mixpanel_headless/types.py` ``` def __post_init__(self) -> None: """Validate construction arguments. Raises: ValueError: If event is empty or contains control characters (FS1). """ _validate_event_name(self.event, "FunnelStep") ``` ## mixpanel_headless.Exclusion ``` Exclusion(event: str, from_step: int = 0, to_step: int | None = None) ``` An event to exclude between funnel steps. Users who perform the excluded event within the specified step range are removed from the funnel. Use plain strings for full-range exclusions; use `Exclusion` objects when you need to target specific step ranges. | ATTRIBUTE | DESCRIPTION | | ----------- | ------------------------------------------------------------------------------------------------------------ | | `event` | Event name to exclude between steps. **TYPE:** `str` | | `from_step` | Start of exclusion range (0-indexed, inclusive). Defaults to 0 (first step). **TYPE:** `int` | | `to_step` | End of exclusion range (0-indexed, inclusive). None means up to the last step in the funnel. **TYPE:** \`int | Example ``` from mixpanel_headless import Exclusion # Exclude between all steps (same as using string "Logout") ex1 = Exclusion("Logout") # Exclude only between steps 1 and 2 ex2 = Exclusion("Refund", from_step=1, to_step=2) ws.query_funnel( ["Signup", "Add to Cart", "Purchase"], exclusions=[ex1, ex2], ) ``` ### event ``` event: str ``` Event name to exclude between steps. ### from_step ``` from_step: int = 0 ``` Start of exclusion range (0-indexed, inclusive). ### to_step ``` to_step: int | None = None ``` End of exclusion range (0-indexed, inclusive). None = last step. ### __post_init__ ``` __post_init__() -> None ``` Validate construction arguments. | RAISES | DESCRIPTION | | ------------ | ----------------------------------------------------------------------------------- | | `ValueError` | If event is empty (EX1), from_step is negative (EX2), or to_step < from_step (EX3). | Source code in `src/mixpanel_headless/types.py` ``` def __post_init__(self) -> None: """Validate construction arguments. Raises: ValueError: If event is empty (EX1), from_step is negative (EX2), or to_step < from_step (EX3). """ _validate_event_name(self.event, "Exclusion") if self.from_step < 0: raise ValueError(f"Exclusion.from_step must be >= 0, got {self.from_step}") if self.to_step is not None and self.to_step < self.from_step: raise ValueError( f"Exclusion.to_step ({self.to_step}) must be >= " f"from_step ({self.from_step})" ) ``` ## mixpanel_headless.HoldingConstant ``` HoldingConstant( property: str, resource_type: Literal["events", "people"] = "events" ) ``` A property to hold constant across all funnel steps. When a property is held constant, only users whose property value is the same at every funnel step are counted as converting. For example, holding `"platform"` constant means a user who signed up on iOS but purchased on web is not counted as converting. | ATTRIBUTE | DESCRIPTION | | --------------- | --------------------------------------------------------------------------------------------------------------------------- | | `property` | Property name to hold constant across steps. **TYPE:** `str` | | `resource_type` | Whether this is an event property or a user-profile property. Defaults to "events". **TYPE:** `Literal['events', 'people']` | Example ``` from mixpanel_headless import HoldingConstant # Hold an event property constant (default) hc1 = HoldingConstant("platform") # Hold a user-profile property constant hc2 = HoldingConstant("plan_tier", resource_type="people") ws.query_funnel( ["Signup", "Purchase"], holding_constant=[hc1, hc2], ) ``` ### property ``` property: str ``` Property name to hold constant across steps. ### resource_type ``` resource_type: Literal['events', 'people'] = 'events' ``` Whether this is an event property or user-profile property. ### __post_init__ ``` __post_init__() -> None ``` Validate construction arguments. | RAISES | DESCRIPTION | | ------------ | --------------------------- | | `ValueError` | If property is empty (HC1). | Source code in `src/mixpanel_headless/types.py` ``` def __post_init__(self) -> None: """Validate construction arguments. Raises: ValueError: If property is empty (HC1). """ if not self.property or not self.property.strip(): raise ValueError("HoldingConstant.property must be a non-empty string") ``` ## mixpanel_headless.FunnelQueryResult ``` FunnelQueryResult( computed_at: str, from_date: str, to_date: str, steps_data: list[dict[str, Any]] = list(), series: dict[str, Any] = dict(), params: dict[str, Any] = dict(), meta: dict[str, Any] = dict(), *, _df_cache: DataFrame | None = None, ) ``` Bases: `ResultWithDataFrame` Result of a funnel query via the insights API. Contains step-level conversion data, timing information, the generated bookmark params (for debugging or persisting as a saved report), and a lazy DataFrame conversion. Unlike `FunnelResult` (which wraps the legacy funnel API), this type wraps the richer bookmark-based insights API response and provides additional fields like `avg_time`, `avg_time_from_start`, and the `params` dict. | ATTRIBUTE | DESCRIPTION | | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `computed_at` | When the query was computed (ISO format). **TYPE:** `str` | | `from_date` | Effective start date from the response. **TYPE:** `str` | | `to_date` | Effective end date from the response. **TYPE:** `str` | | `steps_data` | Step-level results. Each dict contains keys: event, count, step_conv_ratio, overall_conv_ratio, avg_time, avg_time_from_start. **TYPE:** `list[dict[str, Any]]` | | `series` | Raw series data from the API (for advanced use). **TYPE:** `dict[str, Any]` | | `params` | Generated bookmark params sent to the API (for debugging or persistence via create_bookmark). **TYPE:** `dict[str, Any]` | | `meta` | Response metadata (e.g. sampling_factor, is_cached). **TYPE:** `dict[str, Any]` | Example ``` result = ws.query_funnel(["Signup", "Purchase"]) # Overall conversion print(result.overall_conversion_rate) # e.g. 0.12 # DataFrame view print(result.df) # step event count step_conv_ratio overall_conv_ratio ... # Save as a report ws.create_bookmark(CreateBookmarkParams( name="Signup β†’ Purchase Funnel", bookmark_type="funnels", params=result.params, )) ``` ### computed_at ``` computed_at: str ``` When the query was computed (ISO format). ### from_date ``` from_date: str ``` Effective start date from the response. ### to_date ``` to_date: str ``` Effective end date from the response. ### steps_data ``` steps_data: list[dict[str, Any]] = field(default_factory=list) ``` Step-level results. Each dict conforms to :class:`FunnelStepData` (event, count, step_conv_ratio, overall_conv_ratio, avg_time, avg_time_from_start). ### series ``` series: dict[str, Any] = field(default_factory=dict) ``` Raw series data from the API. ### params ``` params: dict[str, Any] = field(default_factory=dict) ``` Generated bookmark params sent to API. ### meta ``` meta: dict[str, Any] = field(default_factory=dict) ``` Response metadata. Conforms to :class:`QueryMeta` (sampling_factor, is_cached, computation_time, query_id). ### overall_conversion_rate ``` overall_conversion_rate: float ``` End-to-end conversion rate from first to last step. | RETURNS | DESCRIPTION | | ------- | ------------------------------------------------------ | | `float` | Float between 0.0 and 1.0 representing the fraction of | | `float` | users who completed all funnel steps. Returns 0.0 if | | `float` | steps_data is empty. | ### df ``` df: DataFrame ``` Convert to DataFrame with one row per funnel step. Columns: `step`, `event`, `count`, `step_conv_ratio`, `overall_conv_ratio`, `avg_time`, `avg_time_from_start`. | RETURNS | DESCRIPTION | | ----------- | ------------------------------------------- | | `DataFrame` | Normalized DataFrame with one row per step. | ### to_dict ``` to_dict() -> dict[str, Any] ``` Serialize for JSON output. | RETURNS | DESCRIPTION | | ---------------- | --------------------------------------------- | | `dict[str, Any]` | Dictionary with all FunnelQueryResult fields. | Source code in `src/mixpanel_headless/types.py` ``` def to_dict(self) -> dict[str, Any]: """Serialize for JSON output. Returns: Dictionary with all FunnelQueryResult fields. """ return { "computed_at": self.computed_at, "from_date": self.from_date, "to_date": self.to_date, "steps_data": self.steps_data, "series": self.series, "params": self.params, "meta": self.meta, } ``` ## Retention Query Types Types for `Workspace.query_retention()` β€” typed retention analysis with event pairs, custom buckets, alignment modes, and segmentation. ## mixpanel_headless.RetentionEvent ``` RetentionEvent( event: str, filters: list[Filter] | None = None, filters_combinator: FiltersCombinator = "all", ) ``` An event specification for retention queries. Wraps an event name with optional per-event filters. Use plain event-name strings for simple retention queries. Use `RetentionEvent` objects when you need per-event filter conditions. | ATTRIBUTE | DESCRIPTION | | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `event` | Mixpanel event name. **TYPE:** `str` | | `filters` | Per-event filter conditions. Each Filter restricts which events count. None means no filters. **TYPE:** \`list[Filter] | | `filters_combinator` | How per-event filters combine. "all" requires all filters to match (AND logic). "any" requires any filter to match (OR logic). **TYPE:** `FiltersCombinator` | Example ``` from mixpanel_headless import RetentionEvent, Filter # Simple event (equivalent to just using "Signup" string) born = RetentionEvent("Signup") # Event with per-event filter born = RetentionEvent( "Signup", filters=[Filter.equals("source", "organic")], ) ws.query_retention(born, "Login") ``` ### event ``` event: str ``` Mixpanel event name. ### filters ``` filters: list[Filter] | None = None ``` Per-event filter conditions. ### filters_combinator ``` filters_combinator: FiltersCombinator = 'all' ``` How per-event filters combine (AND/OR). ### __post_init__ ``` __post_init__() -> None ``` Validate construction arguments. | RAISES | DESCRIPTION | | ------------ | ------------------------------------------------------- | | `ValueError` | If event is empty or contains control characters (RE1). | Source code in `src/mixpanel_headless/types.py` ``` def __post_init__(self) -> None: """Validate construction arguments. Raises: ValueError: If event is empty or contains control characters (RE1). """ _validate_event_name(self.event, "RetentionEvent") ``` ## mixpanel_headless.RetentionAlignment ``` RetentionAlignment = Literal['birth', 'interval_start'] ``` Retention alignment mode. +------------------+----------------------------------------------+ | Value | Meaning | +==================+==============================================+ | birth | Align to each cohort's born date (default) | +------------------+----------------------------------------------+ | interval_start | Align all cohorts to the same start date | +------------------+----------------------------------------------+ ## mixpanel_headless.RetentionMode ``` RetentionMode = Literal['curve', 'trends', 'table'] ``` Display mode for retention query results. +--------+----------------------------------------------+ | Value | Meaning | +========+==============================================+ | curve | Retention curve (default) | +--------+----------------------------------------------+ | trends | Trend lines over time | +--------+----------------------------------------------+ | table | Tabular cohort x bucket grid | +--------+----------------------------------------------+ ## mixpanel_headless.RetentionMathType ``` RetentionMathType = Literal['retention_rate', 'unique', 'total', 'average'] ``` Aggregation function for retention query metrics. +----------------+----------------------------------------------+ | Value | Meaning | +================+==============================================+ | retention_rate | Percentage of users retained (default) | +----------------+----------------------------------------------+ | unique | Raw count of retained users | +----------------+----------------------------------------------+ | total | Total event count per retention bucket | +----------------+----------------------------------------------+ | average | Average of a numeric property per bucket | +----------------+----------------------------------------------+ Maps directly to the `measurement.math` field in bookmark JSON. ## mixpanel_headless.RetentionQueryResult ``` RetentionQueryResult( computed_at: str, from_date: str, to_date: str, cohorts: dict[str, dict[str, Any]] = dict(), average: dict[str, Any] = dict(), params: dict[str, Any] = dict(), meta: dict[str, Any] = dict(), segments: dict[str, dict[str, dict[str, Any]]] = dict(), segment_averages: dict[str, dict[str, Any]] = dict(), *, _df_cache: DataFrame | None = None, ) ``` Bases: `ResultWithDataFrame` Result of a retention query via the insights API. Contains cohort-level retention data, the generated bookmark params (for debugging or persisting as a saved report), and a lazy DataFrame conversion. Supports both unsegmented and segmented (`group_by`) queries. | ATTRIBUTE | DESCRIPTION | | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `computed_at` | When the query was computed (ISO format). **TYPE:** `str` | | `from_date` | Effective start date from the response. **TYPE:** `str` | | `to_date` | Effective end date from the response. **TYPE:** `str` | | `cohorts` | Aggregate cohort-level retention data. Keys are cohort date strings (YYYY-MM-DD), values are dicts with first (cohort size), counts (list of retained user counts per bucket), and rates (list of retention rates per bucket). For segmented queries, this contains the $overall aggregate. **TYPE:** `dict[str, dict[str, Any]]` | | `average` | Synthetic $average cohort data. Same structure as individual cohort entries. **TYPE:** `dict[str, Any]` | | `params` | Generated bookmark params sent to the API (for debugging or persistence via create_bookmark). **TYPE:** `dict[str, Any]` | | `meta` | Response metadata (e.g. sampling_factor, is_cached). **TYPE:** `dict[str, Any]` | | `segments` | Per-segment cohort data. Maps segment name to a dict of cohort_date β†’ {first, counts, rates}. Empty for unsegmented queries. **TYPE:** `dict[str, dict[str, dict[str, Any]]]` | | `segment_averages` | Per-segment $average cohort data. Maps segment name to {first, counts, rates}. Empty for unsegmented queries. **TYPE:** `dict[str, dict[str, Any]]` | Example ``` # Unsegmented retention result = ws.query_retention("Signup", "Login") print(result.df) # cohort_date bucket count rate # Segmented retention result = ws.query_retention( "Signup", "Login", group_by="platform" ) print(result.df) # segment cohort_date bucket count rate for name, cohorts in result.segments.items(): print(f"{name}: {len(cohorts)} cohorts") ``` ### computed_at ``` computed_at: str ``` When the query was computed (ISO format). ### from_date ``` from_date: str ``` Effective start date from the response. ### to_date ``` to_date: str ``` Effective end date from the response. ### cohorts ``` cohorts: dict[str, dict[str, Any]] = field(default_factory=dict) ``` Cohort-level retention data. Each value conforms to :class:`RetentionCohortData` (first, counts, rates). For segmented queries, this contains the `$overall` aggregate. ### average ``` average: dict[str, Any] = field(default_factory=dict) ``` Synthetic $average cohort data. Conforms to :class:`RetentionCohortData`. ### params ``` params: dict[str, Any] = field(default_factory=dict) ``` Generated bookmark params sent to API. ### meta ``` meta: dict[str, Any] = field(default_factory=dict) ``` Response metadata. Conforms to :class:`QueryMeta`. ### segments ``` segments: dict[str, dict[str, dict[str, Any]]] = field(default_factory=dict) ``` Per-segment cohort data. Each inner value conforms to :class:`RetentionCohortData` (first, counts, rates). Empty for unsegmented queries. Populated when `group_by` is used and the API returns breakdown segments alongside `$overall`. ### segment_averages ``` segment_averages: dict[str, dict[str, Any]] = field(default_factory=dict) ``` Per-segment $average cohort data. Each value conforms to :class:`RetentionCohortData`. Empty for unsegmented queries. ### df ``` df: DataFrame ``` Convert to DataFrame with one row per (cohort_date, bucket) pair. For unsegmented queries, columns are: `cohort_date`, `bucket`, `count`, `rate`. For segmented queries (when `segments` is non-empty), columns are: `segment`, `cohort_date`, `bucket`, `count`, `rate`. | RETURNS | DESCRIPTION | | ----------- | ---------------------------------------------------------- | | `DataFrame` | Normalized DataFrame. Empty DataFrame with correct columns | | `DataFrame` | if data is empty. | ### to_dict ``` to_dict() -> dict[str, Any] ``` Serialize for JSON output. | RETURNS | DESCRIPTION | | ---------------- | ------------------------------------------------ | | `dict[str, Any]` | Dictionary with all RetentionQueryResult fields. | | `dict[str, Any]` | Includes segments and segment_averages only | | `dict[str, Any]` | when non-empty. | Source code in `src/mixpanel_headless/types.py` ``` def to_dict(self) -> dict[str, Any]: """Serialize for JSON output. Returns: Dictionary with all RetentionQueryResult fields. Includes ``segments`` and ``segment_averages`` only when non-empty. """ d: dict[str, Any] = { "computed_at": self.computed_at, "from_date": self.from_date, "to_date": self.to_date, "cohorts": self.cohorts, "average": self.average, "params": self.params, "meta": self.meta, } if self.segments: d["segments"] = self.segments if self.segment_averages: d["segment_averages"] = self.segment_averages return d ``` ## Flow Query Types Types for `Workspace.query_flow()` β€” typed flow path analysis with step definitions, direction controls, and visualization modes. ## mixpanel_headless.FlowStep ``` FlowStep( event: str, forward: int | None = None, reverse: int | None = None, label: str | None = None, filters: list[Filter] | None = None, filters_combinator: FiltersCombinator = "all", session_event: FlowSessionEvent | None = None, ) ``` An anchor event in a flow query with per-step configuration. Each flow step identifies a specific event and optional constraints (forward/reverse step counts, filters) that define a node in the flow analysis. | ATTRIBUTE | DESCRIPTION | | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `event` | The event name to anchor this step on. **TYPE:** `str` | | `forward` | Maximum number of forward steps to trace from this event. None means use the query-level default. **TYPE:** \`int | | `reverse` | Maximum number of reverse steps to trace from this event. None means use the query-level default. **TYPE:** \`int | | `label` | Optional display label for this step. If None, the event name is used as the label. **TYPE:** \`str | | `filters` | Optional list of Filter conditions to narrow the events matching this step. None means no per-step filtering. **TYPE:** \`list[Filter] | | `filters_combinator` | How to combine multiple filters β€” "all" requires every filter to match (AND), "any" requires at least one (OR). Defaults to "all". **TYPE:** `FiltersCombinator` | | `session_event` | Optional session anchor type β€” "start" or "end". When set, the event field must match the corresponding session event name ("$session_start" or "$session_end"). None means this is a regular event step. Default: None. **TYPE:** \`FlowSessionEvent | Example ``` step = FlowStep( "Purchase", forward=5, reverse=3, label="Buy", filters=[Filter.equals("country", "US")], filters_combinator="all", ) # Session anchor step session_step = FlowStep( "$session_start", session_event="start", ) ``` ### __post_init__ ``` __post_init__() -> None ``` Validate construction arguments. | RAISES | DESCRIPTION | | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | `ValueError` | If event is empty or contains control characters (FL1), forward/reverse is outside 0-5 range (FL2), or session_event conflicts with event name (FS1). | Source code in `src/mixpanel_headless/types.py` ``` def __post_init__(self) -> None: """Validate construction arguments. Raises: ValueError: If event is empty or contains control characters (FL1), forward/reverse is outside 0-5 range (FL2), or session_event conflicts with event name (FS1). """ _validate_event_name(self.event, "FlowStep") if self.forward is not None and not 0 <= self.forward <= 5: raise ValueError( f"FlowStep.forward must be in range 0-5, got {self.forward}" ) if self.reverse is not None and not 0 <= self.reverse <= 5: raise ValueError( f"FlowStep.reverse must be in range 0-5, got {self.reverse}" ) # FS1: Validate session_event / event consistency if self.session_event is not None: expected_event = ( "$session_start" if self.session_event == "start" else "$session_end" ) if self.event != expected_event: raise ValueError( f"FlowStep.session_event={self.session_event!r} requires " f"event={expected_event!r}, got {self.event!r}" ) ``` ## mixpanel_headless.FlowTreeNode ``` FlowTreeNode( event: str, type: FlowNodeType, step_number: int, total_count: int, drop_off_count: int = 0, converted_count: int = 0, anchor_type: FlowAnchorType = "NORMAL", is_computed: bool = False, children: tuple[FlowTreeNode, ...] = (), time_percentiles_from_start: dict[str, Any] = dict(), time_percentiles_from_prev: dict[str, Any] = dict(), ) ``` A node in a recursive flow prefix tree. Represents a single event in a flow path tree returned by the Mixpanel Flows API when using `mode="tree"`. Each node tracks aggregate counts (total, drop-off, converted) and optionally timing percentiles. Children represent subsequent events in the flow. The tree preserves full path context β€” unlike the sankey graph which merges nodes at the same step position, each tree node is unique to its specific path from root. | ATTRIBUTE | DESCRIPTION | | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | | `event` | The event name at this position in the flow. **TYPE:** `str` | | `type` | Node type β€” "ANCHOR", "NORMAL", "DROPOFF", "PRUNED", "FORWARD", or "REVERSE". **TYPE:** `FlowNodeType` | | `step_number` | Zero-based step index in the flow. **TYPE:** `int` | | `total_count` | Total number of users reaching this node. **TYPE:** `int` | | `drop_off_count` | Number of users who dropped off at this node. **TYPE:** `int` | | `converted_count` | Number of users who continued past this node. **TYPE:** `int` | | `anchor_type` | Anchor classification β€” "NORMAL", "RELATIVE_REVERSE", or "RELATIVE_FORWARD". **TYPE:** `FlowAnchorType` | | `is_computed` | Whether this is a computed/custom event. **TYPE:** `bool` | | `children` | Child nodes representing subsequent events. Defaults to an empty tuple. **TYPE:** `tuple[FlowTreeNode, ...]` | | `time_percentiles_from_start` | Timing percentile data from flow start to this node. Empty dict if timing data is not enabled. **TYPE:** `dict[str, Any]` | | `time_percentiles_from_prev` | Timing percentile data from the previous node to this node. Empty dict if timing data is not enabled. **TYPE:** `dict[str, Any]` | Example ``` root = FlowTreeNode( event="Login", type="ANCHOR", step_number=0, total_count=1000, drop_off_count=50, converted_count=950, children=( FlowTreeNode( event="Search", type="NORMAL", step_number=1, total_count=600, ), ), ) root.depth # 1 root.conversion_rate # 0.95 root.all_paths() # [[root, search_node]] ``` ### depth ``` depth: int ``` Maximum depth of the subtree rooted at this node. A leaf node has depth 0. A node with one level of children has depth 1, and so on. | RETURNS | DESCRIPTION | | ------- | ------------------------------------------------------- | | `int` | Non-negative integer representing the longest path from | | `int` | this node to any leaf descendant. | Example ``` leaf = FlowTreeNode( event="Purchase", type="ANCHOR", step_number=0, total_count=100, ) leaf.depth # 0 ``` ### node_count ``` node_count: int ``` Total number of nodes in the subtree including this node. | RETURNS | DESCRIPTION | | ------- | ------------------------------- | | `int` | Positive integer (always >= 1). | Example ``` node.node_count # 7 ``` ### leaf_count ``` leaf_count: int ``` Number of leaf nodes (nodes with no children) in the subtree. | RETURNS | DESCRIPTION | | ------- | ------------------------------- | | `int` | Positive integer (always >= 1). | Example ``` node.leaf_count # 4 ``` ### conversion_rate ``` conversion_rate: float ``` Fraction of users who converted at this node. Computed as `converted_count / total_count`. Returns `0.0` when `total_count` is zero to avoid division errors. | RETURNS | DESCRIPTION | | ------- | -------------------- | | `float` | Float in [0.0, 1.0]. | Example ``` node.conversion_rate # 0.95 ``` ### drop_off_rate ``` drop_off_rate: float ``` Fraction of users who dropped off at this node. Computed as `drop_off_count / total_count`. Returns `0.0` when `total_count` is zero to avoid division errors. | RETURNS | DESCRIPTION | | ------- | -------------------- | | `float` | Float in [0.0, 1.0]. | Example ``` node.drop_off_rate # 0.05 ``` ### all_paths ``` all_paths() -> list[list[FlowTreeNode]] ``` Return all root-to-leaf paths through this subtree. Each path is a list of `FlowTreeNode` objects from this node down to a leaf, preserving the full node chain so callers can inspect counts, rates, and timing along each path. | RETURNS | DESCRIPTION | | -------------------------- | ------------------------------------------------------ | | `list[list[FlowTreeNode]]` | List of paths, where each path is a list of nodes. The | | `list[list[FlowTreeNode]]` | number of paths equals leaf_count. | Example ``` for path in root.all_paths(): events = [n.event for n in path] print(" -> ".join(events)) # Login -> Search -> Purchase # Login -> Search -> DROPOFF # Login -> Browse -> Purchase # Login -> DROPOFF ``` Source code in `src/mixpanel_headless/types.py` ```` def all_paths(self) -> list[list[FlowTreeNode]]: """Return all root-to-leaf paths through this subtree. Each path is a list of ``FlowTreeNode`` objects from this node down to a leaf, preserving the full node chain so callers can inspect counts, rates, and timing along each path. Returns: List of paths, where each path is a list of nodes. The number of paths equals ``leaf_count``. Example: ```python for path in root.all_paths(): events = [n.event for n in path] print(" -> ".join(events)) # Login -> Search -> Purchase # Login -> Search -> DROPOFF # Login -> Browse -> Purchase # Login -> DROPOFF ``` """ if not self.children: return [[self]] paths: list[list[FlowTreeNode]] = [] for child in self.children: for child_path in child.all_paths(): paths.append([self, *child_path]) return paths ```` ### find ``` find(event: str) -> list[FlowTreeNode] ``` Find all nodes matching an event name via depth-first search. | PARAMETER | DESCRIPTION | | --------- | --------------------------------------------- | | `event` | The event name to search for. **TYPE:** `str` | | RETURNS | DESCRIPTION | | -------------------- | ---------------------------------------------------- | | `list[FlowTreeNode]` | List of matching FlowTreeNode objects. Empty list if | | `list[FlowTreeNode]` | no nodes match. | Example ``` purchases = root.find("Purchase") # [FlowTreeNode(event="Purchase", ...), ...] ``` Source code in `src/mixpanel_headless/types.py` ```` def find(self, event: str) -> list[FlowTreeNode]: """Find all nodes matching an event name via depth-first search. Args: event: The event name to search for. Returns: List of matching ``FlowTreeNode`` objects. Empty list if no nodes match. Example: ```python purchases = root.find("Purchase") # [FlowTreeNode(event="Purchase", ...), ...] ``` """ results: list[FlowTreeNode] = [] if self.event == event: results.append(self) for child in self.children: results.extend(child.find(event)) return results ```` ### flatten ``` flatten() -> list[FlowTreeNode] ``` Return all nodes in pre-order (depth-first) traversal. The root node appears first, followed by its children's subtrees in order. | RETURNS | DESCRIPTION | | -------------------- | ----------------------------------------------- | | `list[FlowTreeNode]` | List of all nodes in the subtree. Length equals | | `list[FlowTreeNode]` | node_count. | Example ``` for node in root.flatten(): print(f"{node.event}: {node.total_count}") ``` Source code in `src/mixpanel_headless/types.py` ```` def flatten(self) -> list[FlowTreeNode]: """Return all nodes in pre-order (depth-first) traversal. The root node appears first, followed by its children's subtrees in order. Returns: List of all nodes in the subtree. Length equals ``node_count``. Example: ```python for node in root.flatten(): print(f"{node.event}: {node.total_count}") ``` """ result: list[FlowTreeNode] = [self] for child in self.children: result.extend(child.flatten()) return result ```` ### to_dict ``` to_dict() -> dict[str, Any] ``` Serialize the tree node recursively to a dictionary. | RETURNS | DESCRIPTION | | ---------------- | ----------------------------------------------------- | | `dict[str, Any]` | Dictionary with all node attributes and recursively | | `dict[str, Any]` | serialized children. Suitable for JSON serialization. | Example ``` d = node.to_dict() d["event"] # "Login" d["children"] # [{"event": "Search", ...}, ...] ``` Source code in `src/mixpanel_headless/types.py` ```` def to_dict(self) -> dict[str, Any]: """Serialize the tree node recursively to a dictionary. Returns: Dictionary with all node attributes and recursively serialized children. Suitable for JSON serialization. Example: ```python d = node.to_dict() d["event"] # "Login" d["children"] # [{"event": "Search", ...}, ...] ``` """ return { "event": self.event, "type": self.type, "step_number": self.step_number, "total_count": self.total_count, "drop_off_count": self.drop_off_count, "converted_count": self.converted_count, "anchor_type": self.anchor_type, "is_computed": self.is_computed, "children": [c.to_dict() for c in self.children], "time_percentiles_from_start": self.time_percentiles_from_start, "time_percentiles_from_prev": self.time_percentiles_from_prev, } ```` ### render ``` render(_prefix: str = '', _is_last: bool = True, _is_root: bool = True) -> str ``` Render the tree as an ASCII string for debugging. Uses box-drawing characters (`β”œβ”€β”€`, `└──`, `β”‚`) to display the tree hierarchy with event names and counts. | PARAMETER | DESCRIPTION | | ---------- | ------------------------------------------------------------------------------------------------------------------------------ | | `_prefix` | Internal prefix for recursive indentation. Do not pass this argument directly. **TYPE:** `str` **DEFAULT:** `''` | | `_is_last` | Internal flag for connector selection. Do not pass this argument directly. **TYPE:** `bool` **DEFAULT:** `True` | | `_is_root` | Internal flag distinguishing the root call from recursive children. Do not pass directly. **TYPE:** `bool` **DEFAULT:** `True` | | RETURNS | DESCRIPTION | | ------- | --------------------------------------------- | | `str` | Multi-line string representation of the tree. | Example ``` print(root.render()) # Login (1000) # β”œβ”€β”€ Search (600) # β”‚ β”œβ”€β”€ Purchase (400) # β”‚ └── DROPOFF (100) # β”œβ”€β”€ Browse (300) # β”‚ └── Purchase (200) # └── DROPOFF (50) ``` Source code in `src/mixpanel_headless/types.py` ```` def render( self, _prefix: str = "", _is_last: bool = True, _is_root: bool = True, ) -> str: """Render the tree as an ASCII string for debugging. Uses box-drawing characters (``\u251c\u2500\u2500``, ``\u2514\u2500\u2500``, ``\u2502``) to display the tree hierarchy with event names and counts. Args: _prefix: Internal prefix for recursive indentation. Do not pass this argument directly. _is_last: Internal flag for connector selection. Do not pass this argument directly. _is_root: Internal flag distinguishing the root call from recursive children. Do not pass directly. Returns: Multi-line string representation of the tree. Example: ```python print(root.render()) # Login (1000) # \u251c\u2500\u2500 Search (600) # \u2502 \u251c\u2500\u2500 Purchase (400) # \u2502 \u2514\u2500\u2500 DROPOFF (100) # \u251c\u2500\u2500 Browse (300) # \u2502 \u2514\u2500\u2500 Purchase (200) # \u2514\u2500\u2500 DROPOFF (50) ``` """ if _is_root: line = f"{self.event} ({self.total_count})\n" child_prefix = "" else: connector = "\u2514\u2500\u2500 " if _is_last else "\u251c\u2500\u2500 " line = f"{_prefix}{connector}{self.event} ({self.total_count})\n" child_prefix = _prefix + (" " if _is_last else "\u2502 ") for i, child in enumerate(self.children): is_last_child = i == len(self.children) - 1 line += child.render( _prefix=child_prefix, _is_last=is_last_child, _is_root=False ) return line ```` ### to_anytree ``` to_anytree() -> Any ``` Convert to an `anytree.AnyNode` tree with parent references. Creates a parallel anytree representation of this subtree. Each anytree node carries the same attributes (event, type, counts, etc.) and gains parent references, path resolution, and rendering capabilities from the anytree library. | RETURNS | DESCRIPTION | | ------- | ------------------------------------------------------- | | `Any` | An anytree.AnyNode root with the full subtree attached. | | `Any` | Use node.parent, node.path, node.children, | | `Any` | and anytree.RenderTree for navigation and display. | Example ``` from anytree import RenderTree, findall at = root.to_anytree() print(RenderTree(at)) # Parent references purchase = findall(at, filter_=lambda n: n.event == "Purchase")[0] purchase.parent.event # "Search" [n.event for n in purchase.path] # ["Login", "Search", "Purchase"] ``` Source code in `src/mixpanel_headless/types.py` ```` def to_anytree(self) -> Any: """Convert to an ``anytree.AnyNode`` tree with parent references. Creates a parallel anytree representation of this subtree. Each anytree node carries the same attributes (event, type, counts, etc.) and gains parent references, path resolution, and rendering capabilities from the anytree library. Returns: An ``anytree.AnyNode`` root with the full subtree attached. Use ``node.parent``, ``node.path``, ``node.children``, and ``anytree.RenderTree`` for navigation and display. Example: ```python from anytree import RenderTree, findall at = root.to_anytree() print(RenderTree(at)) # Parent references purchase = findall(at, filter_=lambda n: n.event == "Purchase")[0] purchase.parent.event # "Search" [n.event for n in purchase.path] # ["Login", "Search", "Purchase"] ``` """ return self._build_anytree_node(parent=None) ```` ## mixpanel_headless.FlowQueryResult ``` FlowQueryResult( computed_at: str, steps: list[dict[str, Any]] = list(), flows: list[dict[str, Any]] = list(), breakdowns: list[dict[str, Any]] = list(), overall_conversion_rate: float = 0.0, params: dict[str, Any] = dict(), meta: dict[str, Any] = dict(), mode: Literal["sankey", "paths", "tree"] = "sankey", trees: list[FlowTreeNode] = list(), *, _df_cache: DataFrame | None = None, _nodes_df_cache: DataFrame | None = None, _edges_df_cache: DataFrame | None = None, _graph_cache: DiGraph[str] | None = None, _trees_df_cache: DataFrame | None = None, _anytree_cache: list[object] | None = None, ) ``` Bases: `ResultWithDataFrame` Result of an ad-hoc flow query. Holds the raw flow analysis data returned by the Mixpanel API, including step nodes, flow edges, breakdowns, and overall conversion. | ATTRIBUTE | DESCRIPTION | | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `computed_at` | ISO-8601 timestamp when the query was computed. **TYPE:** `str` | | `steps` | List of step-node dicts from the API response. **TYPE:** `list[dict[str, Any]]` | | `flows` | List of flow-edge dicts describing transitions between steps. **TYPE:** `list[dict[str, Any]]` | | `breakdowns` | List of breakdown dicts when a breakdown property is used. **TYPE:** `list[dict[str, Any]]` | | `overall_conversion_rate` | Overall conversion rate across the flow (0.0 to 1.0). **TYPE:** `float` | | `params` | The query parameters that produced this result. **TYPE:** `dict[str, Any]` | | `meta` | API metadata (sampling factor, request timing, etc.). **TYPE:** `dict[str, Any]` | | `mode` | The flow visualization mode β€” "sankey" for Sankey diagrams, "paths" for top-paths analysis, or "tree" for prefix tree analysis. **TYPE:** `Literal['sankey', 'paths', 'tree']` | Example ``` result = FlowQueryResult( computed_at="2025-01-15T10:00:00", steps=[{"event": "Login", "count": 100}], flows=[{"path": ["Login", "Purchase"], "count": 30}], overall_conversion_rate=0.3, ) result.to_dict() # {"computed_at": "2025-01-15T10:00:00", ...} ``` ### steps ``` steps: list[dict[str, Any]] = field(default_factory=list) ``` Step-node dicts. Each conforms to :class:`FlowStepNode`. ### flows ``` flows: list[dict[str, Any]] = field(default_factory=list) ``` Flow-edge dicts. Each conforms to :class:`FlowEdge`. ### meta ``` meta: dict[str, Any] = field(default_factory=dict) ``` Response metadata. Conforms to :class:`QueryMeta`. ### nodes_df ``` nodes_df: DataFrame ``` Extract a flat DataFrame of nodes from sankey step data. Each row represents a single node in the flow graph, with columns for step index, event name, node type, count, anchor type, custom event flag, and conversion rate change. The `totalCount` field in the API response is a string and is parsed to `int` here. | RETURNS | DESCRIPTION | | ----------- | ------------------------------------------------------- | | `DataFrame` | DataFrame with columns: step, event, type, | | `DataFrame` | count, anchor_type, is_custom_event, | | `DataFrame` | conversion_rate_change. Returns an empty DataFrame with | | `DataFrame` | the correct columns when steps is empty. | Example ``` result = workspace.query_flow(steps=[FlowStep("Login")]) result.nodes_df # step event type count anchor_type ... # 0 0 Login ANCHOR 100 NORMAL ... ``` ### edges_df ``` edges_df: DataFrame ``` Extract a flat DataFrame of edges from sankey step data. Each row represents a directed edge between two nodes in the flow graph, with columns for source step/event, target step/event, edge count, and target node type. The `totalCount` field in the API response is a string and is parsed to `int` here. | RETURNS | DESCRIPTION | | ----------- | -------------------------------------------------------- | | `DataFrame` | DataFrame with columns: source_step, source_event, | | `DataFrame` | target_step, target_event, count, target_type. | | `DataFrame` | Returns an empty DataFrame with the correct columns when | | `DataFrame` | steps is empty. | Example ``` result = workspace.query_flow(steps=[FlowStep("Login")]) result.edges_df # source_step source_event target_step target_event count target_type # 0 0 Login 1 Search 80 NORMAL ``` ### graph ``` graph: DiGraph ``` Build a networkx directed graph from sankey step data. Nodes are keyed as `"{event}@{step}"` to distinguish the same event appearing at different steps (e.g. `"Login@0"` vs `"Login@2"`). Each node carries `step`, `event`, `type`, `count`, and `anchor_type` attributes. Each edge carries `count` and `type` attributes. The graph is lazily constructed on first access and cached for subsequent calls. | RETURNS | DESCRIPTION | | --------- | ---------------------------------------------------- | | `DiGraph` | A networkx.DiGraph representing the flow. Returns an | | `DiGraph` | empty graph when steps is empty. | Example ``` result = workspace.query_flow(steps=[FlowStep("Login")]) G = result.graph G.nodes["Login@0"]["count"] # 100 ``` ### df ``` df: DataFrame ``` Mode-aware DataFrame from flow data. For `sankey` mode, returns the same DataFrame as `nodes_df` (one row per node with step, event, type, count, etc.). For `paths` mode, returns a tabular DataFrame with one row per step in each flow path, including `path_index`, `step`, `event`, `type`, and `count` columns. | RETURNS | DESCRIPTION | | ----------- | ---------------------------------------------------------- | | `DataFrame` | DataFrame built from nodes (sankey) or flow paths (paths). | | `DataFrame` | Returns an empty DataFrame if no data is available. | Example ``` result = workspace.query_flow( steps=[FlowStep("Login")], mode="sankey" ) result.df.columns # Index(['step', 'event', 'type', 'count', ...]) ``` ### anytree ``` anytree: list[Any] ``` Lazily-cached list of `anytree.AnyNode` roots from tree data. Each `FlowTreeNode` in `trees` is converted to an anytree node tree via `to_anytree()`, enabling parent references, path resolution, and `RenderTree` display. | RETURNS | DESCRIPTION | | ----------- | --------------------------------------------------- | | `list[Any]` | List of anytree.AnyNode root nodes. Empty list when | | `list[Any]` | trees is empty. | Example ``` result = ws.query_flow("Login", mode="tree") for root in result.anytree: from anytree import RenderTree print(RenderTree(root)) ``` ### top_transitions ``` top_transitions(n: int = 10) -> list[tuple[str, str, int]] ``` Return the N highest-traffic transitions between events. Uses the edges DataFrame to find the most common transitions, sorted by count descending. | PARAMETER | DESCRIPTION | | --------- | --------------------------------------------------------------------------------------- | | `n` | Maximum number of transitions to return. Default: 10. **TYPE:** `int` **DEFAULT:** `10` | | RETURNS | DESCRIPTION | | ---------------------------- | ------------------------------------------------------- | | `list[tuple[str, str, int]]` | List of (source_node, target_node, count) tuples sorted | | `list[tuple[str, str, int]]` | by count descending, where each node is formatted as | | `list[tuple[str, str, int]]` | "{event}@{step}" (e.g. "Login@0"). Returns empty | | `list[tuple[str, str, int]]` | list if no edges exist. | Example ``` result = ws.query_flow("Login", forward=3) for src, tgt, count in result.top_transitions(n=5): print(f"{src} -> {tgt}: {count}") # Login@0 -> Search@1: 150 ``` Source code in `src/mixpanel_headless/types.py` ```` def top_transitions(self, n: int = 10) -> list[tuple[str, str, int]]: """Return the N highest-traffic transitions between events. Uses the edges DataFrame to find the most common transitions, sorted by count descending. Args: n: Maximum number of transitions to return. Default: 10. Returns: List of (source_node, target_node, count) tuples sorted by count descending, where each node is formatted as ``"{event}@{step}"`` (e.g. ``"Login@0"``). Returns empty list if no edges exist. Example: ```python result = ws.query_flow("Login", forward=3) for src, tgt, count in result.top_transitions(n=5): print(f"{src} -> {tgt}: {count}") # Login@0 -> Search@1: 150 ``` """ edf = self.edges_df if edf.empty: return [] sorted_df = edf.sort_values("count", ascending=False).head(n) return [ (f"{se}@{ss}", f"{te}@{ts}", int(c)) for se, ss, te, ts, c in zip( sorted_df["source_event"], sorted_df["source_step"], sorted_df["target_event"], sorted_df["target_step"], sorted_df["count"], strict=True, ) ] ```` ### drop_off_summary ``` drop_off_summary() -> dict[str, Any] ``` Per-step drop-off counts and rates. Analyzes each step to identify drop-off nodes (type == "DROPOFF") and calculates the drop-off rate relative to total traffic at that step. | RETURNS | DESCRIPTION | | ---------------- | ------------------------------------------------------ | | `dict[str, Any]` | Dict mapping step keys (e.g., "step_0") to dicts with: | | `dict[str, Any]` | total: Total count at that step | | `dict[str, Any]` | dropoff: Count of users who dropped off | | `dict[str, Any]` | rate: Drop-off rate (0.0 to 1.0) | | `dict[str, Any]` | Returns empty dict if no steps exist. | Example ``` result = ws.query_flow("Login", forward=3) for step, info in result.drop_off_summary().items(): print(f"{step}: {info['rate']:.0%} drop-off") ``` Source code in `src/mixpanel_headless/types.py` ```` def drop_off_summary(self) -> dict[str, Any]: """Per-step drop-off counts and rates. Analyzes each step to identify drop-off nodes (type == "DROPOFF") and calculates the drop-off rate relative to total traffic at that step. Returns: Dict mapping step keys (e.g., "step_0") to dicts with: - total: Total count at that step - dropoff: Count of users who dropped off - rate: Drop-off rate (0.0 to 1.0) Returns empty dict if no steps exist. Example: ```python result = ws.query_flow("Login", forward=3) for step, info in result.drop_off_summary().items(): print(f"{step}: {info['rate']:.0%} drop-off") ``` """ if not self.steps: return {} summary: dict[str, Any] = {} for step_idx, step in enumerate(self.steps): total = 0 dropoff = 0 for node in step.get("nodes", []): count = _safe_int(node.get("totalCount", "0")) node_type = node.get("type", "") total += count # Count dropoff edges only from non-DROPOFF nodes. # DROPOFF nodes represent prior-step dropoffs carried # forward; their self-edges would double-count. if node_type != "DROPOFF": for edge in node.get("edges", []): if edge.get("type") == "DROPOFF": dropoff += _safe_int(edge.get("totalCount", "0")) rate = dropoff / total if total > 0 else 0.0 summary[f"step_{step_idx}"] = { "total": total, "dropoff": dropoff, "rate": rate, } return summary ```` ### to_dict ``` to_dict() -> dict[str, Any] ``` Serialize the flow query result for JSON output. | RETURNS | DESCRIPTION | | ---------------- | ------------------------------------------------------- | | `dict[str, Any]` | Dictionary with all FlowQueryResult fields suitable for | | `dict[str, Any]` | JSON serialization. | Source code in `src/mixpanel_headless/types.py` ``` def to_dict(self) -> dict[str, Any]: """Serialize the flow query result for JSON output. Returns: Dictionary with all FlowQueryResult fields suitable for JSON serialization. """ return { "computed_at": self.computed_at, "steps": self.steps, "flows": self.flows, "breakdowns": self.breakdowns, "overall_conversion_rate": self.overall_conversion_rate, "params": self.params, "meta": self.meta, "mode": self.mode, "trees": [t.to_dict() for t in self.trees], } ``` ## Legacy Query Results ## mixpanel_headless.SegmentationResult ``` SegmentationResult( event: str, from_date: str, to_date: str, unit: Literal["day", "week", "month"], segment_property: str | None, total: int, series: dict[str, dict[str, int]] = dict(), *, _df_cache: DataFrame | None = None, ) ``` Bases: `ResultWithDataFrame` Result of a segmentation query. Contains time-series data for an event, optionally segmented by a property. Inherits from ResultWithDataFrame to provide: - Lazy DataFrame caching via \_df_cache field - Normalized table output via to_table_dict() method ### event ``` event: str ``` Queried event name. ### from_date ``` from_date: str ``` Query start date (YYYY-MM-DD). ### to_date ``` to_date: str ``` Query end date (YYYY-MM-DD). ### unit ``` unit: Literal['day', 'week', 'month'] ``` Time unit for aggregation. ### segment_property ``` segment_property: str | None ``` Property used for segmentation (None if total only). ### total ``` total: int ``` Total count across all segments and time periods. ### series ``` series: dict[str, dict[str, int]] = field(default_factory=dict) ``` Time series data by segment. Structure: {segment_name: {date_string: count}} Example: {"US": {"2024-01-01": 150, "2024-01-02": 200}, "EU": {...}} For unsegmented queries, segment_name is "total". ### df ``` df: DataFrame ``` Convert to DataFrame with columns: date, segment, count. For unsegmented queries, segment column is 'total'. ### to_dict ``` to_dict() -> dict[str, Any] ``` Serialize for JSON output. Source code in `src/mixpanel_headless/types.py` ``` def to_dict(self) -> dict[str, Any]: """Serialize for JSON output.""" return { "event": self.event, "from_date": self.from_date, "to_date": self.to_date, "unit": self.unit, "segment_property": self.segment_property, "total": self.total, "series": self.series, } ``` ## mixpanel_headless.FunnelResult ``` FunnelResult( funnel_id: int, funnel_name: str, from_date: str, to_date: str, conversion_rate: float, steps: list[FunnelResultStep] = list(), *, _df_cache: DataFrame | None = None, ) ``` Bases: `ResultWithDataFrame` Result of a funnel query. Contains step-by-step conversion data for a funnel. Inherits from ResultWithDataFrame to provide: - Lazy DataFrame caching via \_df_cache field - Normalized table output via to_table_dict() method ### funnel_id ``` funnel_id: int ``` Funnel identifier. ### funnel_name ``` funnel_name: str ``` Funnel display name. ### from_date ``` from_date: str ``` Query start date. ### to_date ``` to_date: str ``` Query end date. ### conversion_rate ``` conversion_rate: float ``` Overall conversion rate (0.0 to 1.0). ### steps ``` steps: list[FunnelResultStep] = field(default_factory=list) ``` Step-by-step breakdown. ### df ``` df: DataFrame ``` Convert to DataFrame with columns: step, event, count, conversion_rate. ### to_dict ``` to_dict() -> dict[str, Any] ``` Serialize for JSON output. Source code in `src/mixpanel_headless/types.py` ``` def to_dict(self) -> dict[str, Any]: """Serialize for JSON output.""" return { "funnel_id": self.funnel_id, "funnel_name": self.funnel_name, "from_date": self.from_date, "to_date": self.to_date, "conversion_rate": self.conversion_rate, "steps": [step.to_dict() for step in self.steps], } ``` ## mixpanel_headless.FunnelResultStep ``` FunnelResultStep(event: str, count: int, conversion_rate: float) ``` Single step result in a legacy funnel query response. ### event ``` event: str ``` Event name for this step. ### count ``` count: int ``` Number of users at this step. ### conversion_rate ``` conversion_rate: float ``` Conversion rate from previous step (0.0 to 1.0). ### to_dict ``` to_dict() -> dict[str, Any] ``` Serialize for JSON output. Source code in `src/mixpanel_headless/types.py` ``` def to_dict(self) -> dict[str, Any]: """Serialize for JSON output.""" return { "event": self.event, "count": self.count, "conversion_rate": self.conversion_rate, } ``` ## mixpanel_headless.RetentionResult ``` RetentionResult( born_event: str, return_event: str, from_date: str, to_date: str, unit: Literal["day", "week", "month"], cohorts: list[CohortInfo] = list(), *, _df_cache: DataFrame | None = None, ) ``` Bases: `ResultWithDataFrame` Result of a retention query. Contains cohort-based retention data. Inherits from ResultWithDataFrame to provide: - Lazy DataFrame caching via \_df_cache field - Normalized table output via to_table_dict() method ### born_event ``` born_event: str ``` Event that defines cohort membership. ### return_event ``` return_event: str ``` Event that defines return. ### from_date ``` from_date: str ``` Query start date. ### to_date ``` to_date: str ``` Query end date. ### unit ``` unit: Literal['day', 'week', 'month'] ``` Time unit for retention periods. ### cohorts ``` cohorts: list[CohortInfo] = field(default_factory=list) ``` Cohort retention data. ### df ``` df: DataFrame ``` Convert to DataFrame with columns: cohort_date, cohort_size, period_N. ### to_dict ``` to_dict() -> dict[str, Any] ``` Serialize for JSON output. Source code in `src/mixpanel_headless/types.py` ``` def to_dict(self) -> dict[str, Any]: """Serialize for JSON output.""" return { "born_event": self.born_event, "return_event": self.return_event, "from_date": self.from_date, "to_date": self.to_date, "unit": self.unit, "cohorts": [cohort.to_dict() for cohort in self.cohorts], } ``` ## mixpanel_headless.CohortInfo ``` CohortInfo(date: str, size: int, retention: list[float] = list()) ``` Retention data for a single cohort. ### date ``` date: str ``` Cohort date (when users were 'born'). ### size ``` size: int ``` Number of users in cohort. ### retention ``` retention: list[float] = field(default_factory=list) ``` Retention percentages by period (0.0 to 1.0). ### to_dict ``` to_dict() -> dict[str, Any] ``` Serialize for JSON output. Source code in `src/mixpanel_headless/types.py` ``` def to_dict(self) -> dict[str, Any]: """Serialize for JSON output.""" return { "date": self.date, "size": self.size, "retention": self.retention, } ``` ## mixpanel_headless.JQLResult ``` JQLResult(_raw: list[Any] = list(), *, _df_cache: DataFrame | None = None) ``` Bases: `ResultWithDataFrame` Result of a JQL query. JQL (JavaScript Query Language) allows custom queries against Mixpanel data. Inherits from ResultWithDataFrame to provide: - Lazy DataFrame caching via \_df_cache field - Normalized table output via to_table_dict() method The df property intelligently detects JQL result patterns (groupBy, percentiles, simple dicts) and converts them to clean tabular format. ### raw ``` raw: list[Any] ``` Raw result data from JQL execution. ### df ``` df: DataFrame ``` Convert result to DataFrame with intelligent structure detection. The conversion strategy depends on the detected JQL result pattern: **groupBy results** (detected by {key: [...], value: X} structure): - Keys expanded to columns: key_0, key_1, key_2, ... - Single value: "value" column - Multiple reducers (value array): value_0, value_1, value_2, ... - Additional fields (from .map()): preserved as-is - Example: {"key": ["US"], "value": 100, "name": "USA"} -> columns: key_0, value, name **Nested percentile results** (\[[{percentile: X, value: Y}, ...]\]): - Outer list unwrapped, inner dicts converted directly **Simple list of dicts** (already well-structured): - Converted directly to DataFrame preserving all fields **Fallback for other structures** (scalars, mixed types, incompatible dicts): - Safely wrapped in single "value" column to prevent data loss - Used when structure doesn't match known patterns | RAISES | DESCRIPTION | | ------------ | -------------------------------------------------------------------------------------------------------------------------------- | | `ValueError` | If groupBy structure has inconsistent value types across rows (some scalar, some array) which indicates malformed query results. | | RETURNS | DESCRIPTION | | ----------- | ---------------------------------------------------- | | `DataFrame` | DataFrame representation, cached after first access. | ### to_dict ``` to_dict() -> dict[str, Any] ``` Serialize for JSON output. Source code in `src/mixpanel_headless/types.py` ``` def to_dict(self) -> dict[str, Any]: """Serialize for JSON output.""" return { "raw": self._raw, "row_count": len(self._raw), } ``` ## Discovery Types ## mixpanel_headless.FunnelInfo ``` FunnelInfo(funnel_id: int, name: str) ``` A saved funnel definition. Represents a funnel saved in Mixpanel that can be queried using the funnel() method. ### funnel_id ``` funnel_id: int ``` Unique identifier for funnel queries. ### name ``` name: str ``` Human-readable funnel name. ### to_dict ``` to_dict() -> dict[str, Any] ``` Serialize for JSON output. Source code in `src/mixpanel_headless/types.py` ``` def to_dict(self) -> dict[str, Any]: """Serialize for JSON output.""" return { "funnel_id": self.funnel_id, "name": self.name, } ``` ## mixpanel_headless.SavedCohort ``` SavedCohort( id: int, name: str, count: int, description: str, created: str, is_visible: bool, ) ``` A saved cohort definition. Represents a user cohort saved in Mixpanel for profile filtering. ### id ``` id: int ``` Unique identifier for profile filtering. ### name ``` name: str ``` Human-readable cohort name. ### count ``` count: int ``` Current number of users in cohort. ### description ``` description: str ``` Optional description (may be empty string). ### created ``` created: str ``` Creation timestamp (YYYY-MM-DD HH:mm:ss). ### is_visible ``` is_visible: bool ``` Whether cohort is visible in Mixpanel UI. ### to_dict ``` to_dict() -> dict[str, Any] ``` Serialize for JSON output. Source code in `src/mixpanel_headless/types.py` ``` def to_dict(self) -> dict[str, Any]: """Serialize for JSON output.""" return { "id": self.id, "name": self.name, "count": self.count, "description": self.description, "created": self.created, "is_visible": self.is_visible, } ``` ## mixpanel_headless.TopEvent ``` TopEvent(event: str, count: int, percent_change: float) ``` Today's event activity data. Represents an event's current activity including count and trend. | ATTRIBUTE | DESCRIPTION | | ---------------- | ---------------------------------------------------------- | | `event` | Event name. **TYPE:** `str` | | `count` | Today's event count. **TYPE:** `int` | | `percent_change` | Change vs yesterday (-1.0 to +infinity). **TYPE:** `float` | Example ``` top = ws.top_events(limit=10) for t in top: print(f"{t.event}: {t.count:,} ({t.percent_change:+.1%})") ``` ### event ``` event: str ``` Event name. ### count ``` count: int ``` Today's event count. ### percent_change ``` percent_change: float ``` Change vs yesterday (-1.0 to +infinity). ### to_dict ``` to_dict() -> dict[str, Any] ``` Serialize for JSON output. Source code in `src/mixpanel_headless/types.py` ``` def to_dict(self) -> dict[str, Any]: """Serialize for JSON output.""" return { "event": self.event, "count": self.count, "percent_change": self.percent_change, } ``` ## Subproperty Discovery Types Types for `Workspace.subproperties()` β€” schema discovery for list-of-object event properties. See [Subproperties](https://mixpanel.github.io/mixpanel-headless/guide/discovery/#subproperties) for usage. ## mixpanel_headless.SubPropertyInfo ``` SubPropertyInfo( name: str, type: CustomPropertyType, sample_values: tuple[str | int | float | bool, ...], ) ``` Discovered subproperty of a list-of-object event property. Returned by :meth:`Workspace.subproperties` to describe the inner structure of properties whose values are lists of objects (e.g. `cart` is a list of `{"Brand": str, "Category": str, "Price": int}` items). Use the `name` and `type` to construct :meth:`GroupBy.list_item` and :meth:`Filter.list_contains` calls. | ATTRIBUTE | DESCRIPTION | | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | | `name` | Subproperty name as it appears inside each object. **TYPE:** `str` | | `type` | Inferred data type. Mixed sub-value types collapse to "string" (a UserWarning is emitted at discovery time). **TYPE:** `CustomPropertyType` | | `sample_values` | Up to 5 distinct sample values observed across the sampled rows. **TYPE:** \`tuple\[str | Example ``` for sp in ws.subproperties("cart", event="Cart Viewed"): print(sp.name, sp.type, sp.sample_values) # Brand string ('nike', 'puma', 'h&m') # Category string ('hats', 'jeans', 'shoes') # Item ID number (35317, 35318) # Price number (51, 87, 102) ``` ### name ``` name: str ``` Subproperty name as it appears inside each object. ### type ``` type: CustomPropertyType ``` Inferred data type, suitable for `GroupBy.list_item(sub_type=...)`. ### sample_values ``` sample_values: tuple[str | int | float | bool, ...] ``` Up to 5 distinct sample values observed across the sampled rows. ### to_dict ``` to_dict() -> dict[str, Any] ``` Serialize the subproperty info as a plain dict for JSON output. | RETURNS | DESCRIPTION | | ---------------- | -------------------------------------------------- | | `dict[str, Any]` | Dict with name (str), type (str), and | | `dict[str, Any]` | sample_values (list of scalars) keys, suitable for | | `dict[str, Any]` | JSON serialization. | Source code in `src/mixpanel_headless/types.py` ``` def to_dict(self) -> dict[str, Any]: """Serialize the subproperty info as a plain dict for JSON output. Returns: Dict with ``name`` (str), ``type`` (str), and ``sample_values`` (list of scalars) keys, suitable for JSON serialization. """ return { "name": self.name, "type": self.type, "sample_values": list(self.sample_values), } ``` ## Lexicon Types ## mixpanel_headless.LexiconSchema ``` LexiconSchema(entity_type: str, name: str, schema_json: LexiconDefinition) ``` Complete schema definition from Mixpanel Lexicon. Represents a documented event or profile property definition from the Mixpanel data dictionary. ### entity_type ``` entity_type: str ``` Type of entity (e.g., 'event', 'profile', 'custom_event', 'group', etc.). ### name ``` name: str ``` Name of the event or profile property. ### schema_json ``` schema_json: LexiconDefinition ``` Full schema definition. ### to_dict ``` to_dict() -> dict[str, Any] ``` Serialize for JSON output. | RETURNS | DESCRIPTION | | ---------------- | --------------------------------------------------- | | `dict[str, Any]` | Dictionary with entity_type, name, and schema_json. | Source code in `src/mixpanel_headless/types.py` ``` def to_dict(self) -> dict[str, Any]: """Serialize for JSON output. Returns: Dictionary with entity_type, name, and schema_json. """ return { "entity_type": self.entity_type, "name": self.name, "schema_json": self.schema_json.to_dict(), } ``` ## mixpanel_headless.LexiconDefinition ``` LexiconDefinition( description: str | None, properties: dict[str, LexiconProperty], metadata: LexiconMetadata | None, ) ``` Full schema definition for an event or profile property in Lexicon. Contains the structural definition including description, properties, and platform-specific metadata. ### description ``` description: str | None ``` Human-readable description of the entity. ### properties ``` properties: dict[str, LexiconProperty] ``` Property definitions keyed by property name. ### metadata ``` metadata: LexiconMetadata | None ``` Optional Mixpanel-specific metadata for the entity. ### to_dict ``` to_dict() -> dict[str, Any] ``` Serialize for JSON output. | RETURNS | DESCRIPTION | | ---------------- | -------------------------------------------------------------------- | | `dict[str, Any]` | Dictionary with properties, and optionally description and metadata. | Source code in `src/mixpanel_headless/types.py` ``` def to_dict(self) -> dict[str, Any]: """Serialize for JSON output. Returns: Dictionary with properties, and optionally description and metadata. """ result: dict[str, Any] = { "properties": {k: v.to_dict() for k, v in self.properties.items()}, } if self.description is not None: result["description"] = self.description if self.metadata is not None: result["metadata"] = self.metadata.to_dict() return result ``` ## mixpanel_headless.LexiconProperty ``` LexiconProperty( type: str, description: str | None, metadata: LexiconMetadata | None ) ``` Schema definition for a single property in a Lexicon schema. Describes the type and metadata for an event or profile property. ### type ``` type: str ``` JSON Schema type (string, number, boolean, array, object, integer, null). ### description ``` description: str | None ``` Human-readable description of the property. ### metadata ``` metadata: LexiconMetadata | None ``` Optional Mixpanel-specific metadata. ### to_dict ``` to_dict() -> dict[str, Any] ``` Serialize for JSON output. | RETURNS | DESCRIPTION | | ---------------- | -------------------------------------------------------------- | | `dict[str, Any]` | Dictionary with type, and optionally description and metadata. | Source code in `src/mixpanel_headless/types.py` ``` def to_dict(self) -> dict[str, Any]: """Serialize for JSON output. Returns: Dictionary with type, and optionally description and metadata. """ result: dict[str, Any] = {"type": self.type} if self.description is not None: result["description"] = self.description if self.metadata is not None: result["metadata"] = self.metadata.to_dict() return result ``` ## mixpanel_headless.LexiconMetadata ``` LexiconMetadata( source: str | None, display_name: str | None, tags: list[str], hidden: bool, dropped: bool, contacts: list[str], team_contacts: list[str], ) ``` Mixpanel-specific metadata for Lexicon schemas and properties. Contains platform-specific information about how schemas and properties are displayed and organized in the Mixpanel UI. ### source ``` source: str | None ``` Origin of the schema definition (e.g., 'api', 'csv', 'ui'). ### display_name ``` display_name: str | None ``` Human-readable display name in Mixpanel UI. ### tags ``` tags: list[str] ``` Categorization tags for organization. ### hidden ``` hidden: bool ``` Whether hidden from Mixpanel UI. ### dropped ``` dropped: bool ``` Whether data is dropped/ignored. ### contacts ``` contacts: list[str] ``` Owner email addresses. ### team_contacts ``` team_contacts: list[str] ``` Team ownership labels. ### to_dict ``` to_dict() -> dict[str, Any] ``` Serialize for JSON output. | RETURNS | DESCRIPTION | | ---------------- | ------------------------------------ | | `dict[str, Any]` | Dictionary with all metadata fields. | Source code in `src/mixpanel_headless/types.py` ``` def to_dict(self) -> dict[str, Any]: """Serialize for JSON output. Returns: Dictionary with all metadata fields. """ return { "source": self.source, "display_name": self.display_name, "tags": self.tags, "hidden": self.hidden, "dropped": self.dropped, "contacts": self.contacts, "team_contacts": self.team_contacts, } ``` ## Event Analytics Results ## mixpanel_headless.EventCountsResult ``` EventCountsResult( events: list[str], from_date: str, to_date: str, unit: Literal["day", "week", "month"], type: Literal["general", "unique", "average"], series: dict[str, dict[str, int]], *, _df_cache: DataFrame | None = None, ) ``` Bases: `ResultWithDataFrame` Time-series event count data. Contains aggregate counts for multiple events over time with lazy DataFrame conversion support. Inherits from ResultWithDataFrame to provide: - Lazy DataFrame caching via \_df_cache field - Normalized table output via to_table_dict() method ### events ``` events: list[str] ``` Queried event names. ### from_date ``` from_date: str ``` Query start date (YYYY-MM-DD). ### to_date ``` to_date: str ``` Query end date (YYYY-MM-DD). ### unit ``` unit: Literal['day', 'week', 'month'] ``` Time unit for aggregation. ### type ``` type: Literal['general', 'unique', 'average'] ``` Counting method used. ### series ``` series: dict[str, dict[str, int]] ``` Time series data: {event_name: {date: count}}. ### df ``` df: DataFrame ``` Convert to DataFrame with columns: date, event, count. Conversion is lazy - computed on first access and cached. ### to_dict ``` to_dict() -> dict[str, Any] ``` Serialize for JSON output. Source code in `src/mixpanel_headless/types.py` ``` def to_dict(self) -> dict[str, Any]: """Serialize for JSON output.""" return { "events": self.events, "from_date": self.from_date, "to_date": self.to_date, "unit": self.unit, "type": self.type, "series": self.series, } ``` ## mixpanel_headless.PropertyCountsResult ``` PropertyCountsResult( event: str, property_name: str, from_date: str, to_date: str, unit: Literal["day", "week", "month"], type: Literal["general", "unique", "average"], series: dict[str, dict[str, int]], *, _df_cache: DataFrame | None = None, ) ``` Bases: `ResultWithDataFrame` Time-series property value distribution data. Contains aggregate counts by property values over time with lazy DataFrame conversion support. Inherits from ResultWithDataFrame to provide: - Lazy DataFrame caching via \_df_cache field - Normalized table output via to_table_dict() method ### event ``` event: str ``` Queried event name. ### property_name ``` property_name: str ``` Property used for segmentation. ### from_date ``` from_date: str ``` Query start date (YYYY-MM-DD). ### to_date ``` to_date: str ``` Query end date (YYYY-MM-DD). ### unit ``` unit: Literal['day', 'week', 'month'] ``` Time unit for aggregation. ### type ``` type: Literal['general', 'unique', 'average'] ``` Counting method used. ### series ``` series: dict[str, dict[str, int]] ``` Time series data by property value. Structure: {property_value: {date: count}} Example: {"US": {"2024-01-01": 150, "2024-01-02": 200}, "EU": {...}} ### df ``` df: DataFrame ``` Convert to DataFrame with columns: date, value, count. Conversion is lazy - computed on first access and cached. ### to_dict ``` to_dict() -> dict[str, Any] ``` Serialize for JSON output. Source code in `src/mixpanel_headless/types.py` ``` def to_dict(self) -> dict[str, Any]: """Serialize for JSON output.""" return { "event": self.event, "property_name": self.property_name, "from_date": self.from_date, "to_date": self.to_date, "unit": self.unit, "type": self.type, "series": self.series, } ``` ## Advanced Query Results ## mixpanel_headless.UserEvent ``` UserEvent(event: str, time: datetime, properties: dict[str, Any] = dict()) ``` Single event in a user's activity feed. Represents one event from a user's event history with timestamp and all associated properties. ### event ``` event: str ``` Event name. ### time ``` time: datetime ``` Event timestamp (UTC). ### properties ``` properties: dict[str, Any] = field(default_factory=dict) ``` All event properties including system properties. ### to_dict ``` to_dict() -> dict[str, Any] ``` Serialize for JSON output. Source code in `src/mixpanel_headless/types.py` ``` def to_dict(self) -> dict[str, Any]: """Serialize for JSON output.""" return { "event": self.event, "time": self.time.isoformat(), "properties": self.properties, } ``` ## mixpanel_headless.ActivityFeedResult ``` ActivityFeedResult( distinct_ids: list[str], from_date: str | None, to_date: str | None, events: list[UserEvent] = list(), *, _df_cache: DataFrame | None = None, ) ``` Bases: `ResultWithDataFrame` Collection of user events from activity feed query. Contains chronological event history for one or more users with lazy DataFrame conversion support. Inherits from ResultWithDataFrame to provide: - Lazy DataFrame caching via \_df_cache field - Normalized table output via to_table_dict() method ### distinct_ids ``` distinct_ids: list[str] ``` Queried user identifiers. ### from_date ``` from_date: str | None ``` Start date filter (YYYY-MM-DD), None if not specified. ### to_date ``` to_date: str | None ``` End date filter (YYYY-MM-DD), None if not specified. ### events ``` events: list[UserEvent] = field(default_factory=list) ``` Event history (chronological order). ### df ``` df: DataFrame ``` Convert to DataFrame with columns: event, time, distinct_id, + properties. Flattens event properties into individual columns. Conversion is lazy - computed on first access and cached. ### to_dict ``` to_dict() -> dict[str, Any] ``` Serialize for JSON output. Source code in `src/mixpanel_headless/types.py` ``` def to_dict(self) -> dict[str, Any]: """Serialize for JSON output.""" return { "distinct_ids": self.distinct_ids, "from_date": self.from_date, "to_date": self.to_date, "event_count": len(self.events), "events": [e.to_dict() for e in self.events], } ``` ## mixpanel_headless.FrequencyResult ``` FrequencyResult( event: str | None, from_date: str, to_date: str, unit: Literal["day", "week", "month"], addiction_unit: Literal["hour", "day"], data: dict[str, list[int]] = dict(), *, _df_cache: DataFrame | None = None, ) ``` Bases: `ResultWithDataFrame` Event frequency distribution (addiction analysis). Contains frequency arrays showing how many users performed events in N time periods, with lazy DataFrame conversion support. Inherits from ResultWithDataFrame to provide: - Lazy DataFrame caching via \_df_cache field - Normalized table output via to_table_dict() method ### event ``` event: str | None ``` Filtered event name (None = all events). ### from_date ``` from_date: str ``` Query start date (YYYY-MM-DD). ### to_date ``` to_date: str ``` Query end date (YYYY-MM-DD). ### unit ``` unit: Literal['day', 'week', 'month'] ``` Overall time period. ### addiction_unit ``` addiction_unit: Literal['hour', 'day'] ``` Measurement granularity. ### data ``` data: dict[str, list[int]] = field(default_factory=dict) ``` Frequency arrays by date. Structure: {date: [count_1, count_2, ...]} Example: {"2024-01-01": [100, 50, 25, 10]} Each array shows user counts by frequency: - Index 0: users active exactly 1 time - Index 1: users active exactly 2 times - Index N: users active exactly N+1 times ### df ``` df: DataFrame ``` Convert to DataFrame with columns: date, period_1, period_2, ... Each period_N column shows users active in at least N time periods. Conversion is lazy - computed on first access and cached. ### to_dict ``` to_dict() -> dict[str, Any] ``` Serialize for JSON output. Source code in `src/mixpanel_headless/types.py` ``` def to_dict(self) -> dict[str, Any]: """Serialize for JSON output.""" return { "event": self.event, "from_date": self.from_date, "to_date": self.to_date, "unit": self.unit, "addiction_unit": self.addiction_unit, "data": self.data, } ``` ## mixpanel_headless.NumericBucketResult ``` NumericBucketResult( event: str, from_date: str, to_date: str, property_expr: str, unit: Literal["hour", "day"], series: dict[str, dict[str, int]] = dict(), *, _df_cache: DataFrame | None = None, ) ``` Bases: `ResultWithDataFrame` Events segmented into numeric property ranges. Contains time-series data bucketed by automatically determined numeric ranges, with lazy DataFrame conversion support. Inherits from ResultWithDataFrame to provide: - Lazy DataFrame caching via \_df_cache field - Normalized table output via to_table_dict() method ### event ``` event: str ``` Queried event name. ### from_date ``` from_date: str ``` Query start date (YYYY-MM-DD). ### to_date ``` to_date: str ``` Query end date (YYYY-MM-DD). ### property_expr ``` property_expr: str ``` The 'on' expression used for bucketing. ### unit ``` unit: Literal['hour', 'day'] ``` Time aggregation unit. ### series ``` series: dict[str, dict[str, int]] = field(default_factory=dict) ``` Bucket data: {range_string: {date: count}}. ### df ``` df: DataFrame ``` Convert to DataFrame with columns: date, bucket, count. Conversion is lazy - computed on first access and cached. ### to_dict ``` to_dict() -> dict[str, Any] ``` Serialize for JSON output. Source code in `src/mixpanel_headless/types.py` ``` def to_dict(self) -> dict[str, Any]: """Serialize for JSON output.""" return { "event": self.event, "from_date": self.from_date, "to_date": self.to_date, "property_expr": self.property_expr, "unit": self.unit, "series": self.series, } ``` ## mixpanel_headless.NumericSumResult ``` NumericSumResult( event: str, from_date: str, to_date: str, property_expr: str, unit: Literal["hour", "day"], results: dict[str, float] = dict(), computed_at: str | None = None, *, _df_cache: DataFrame | None = None, ) ``` Bases: `ResultWithDataFrame` Sum of numeric property values per time unit. Contains daily or hourly sum totals for a numeric property with lazy DataFrame conversion support. Inherits from ResultWithDataFrame to provide: - Lazy DataFrame caching via \_df_cache field - Normalized table output via to_table_dict() method ### event ``` event: str ``` Queried event name. ### from_date ``` from_date: str ``` Query start date (YYYY-MM-DD). ### to_date ``` to_date: str ``` Query end date (YYYY-MM-DD). ### property_expr ``` property_expr: str ``` The 'on' expression summed. ### unit ``` unit: Literal['hour', 'day'] ``` Time aggregation unit. ### results ``` results: dict[str, float] = field(default_factory=dict) ``` Sum values: {date: sum}. ### computed_at ``` computed_at: str | None = None ``` Computation timestamp (if provided by API). ### df ``` df: DataFrame ``` Convert to DataFrame with columns: date, sum. Conversion is lazy - computed on first access and cached. ### to_dict ``` to_dict() -> dict[str, Any] ``` Serialize for JSON output. Source code in `src/mixpanel_headless/types.py` ``` def to_dict(self) -> dict[str, Any]: """Serialize for JSON output.""" result: dict[str, Any] = { "event": self.event, "from_date": self.from_date, "to_date": self.to_date, "property_expr": self.property_expr, "unit": self.unit, "results": self.results, } if self.computed_at is not None: result["computed_at"] = self.computed_at return result ``` ## mixpanel_headless.NumericAverageResult ``` NumericAverageResult( event: str, from_date: str, to_date: str, property_expr: str, unit: Literal["hour", "day"], results: dict[str, float] = dict(), *, _df_cache: DataFrame | None = None, ) ``` Bases: `ResultWithDataFrame` Average of numeric property values per time unit. Contains daily or hourly average values for a numeric property with lazy DataFrame conversion support. Inherits from ResultWithDataFrame to provide: - Lazy DataFrame caching via \_df_cache field - Normalized table output via to_table_dict() method ### event ``` event: str ``` Queried event name. ### from_date ``` from_date: str ``` Query start date (YYYY-MM-DD). ### to_date ``` to_date: str ``` Query end date (YYYY-MM-DD). ### property_expr ``` property_expr: str ``` The 'on' expression averaged. ### unit ``` unit: Literal['hour', 'day'] ``` Time aggregation unit. ### results ``` results: dict[str, float] = field(default_factory=dict) ``` Average values: {date: average}. ### df ``` df: DataFrame ``` Convert to DataFrame with columns: date, average. Conversion is lazy - computed on first access and cached. ### to_dict ``` to_dict() -> dict[str, Any] ``` Serialize for JSON output. Source code in `src/mixpanel_headless/types.py` ``` def to_dict(self) -> dict[str, Any]: """Serialize for JSON output.""" return { "event": self.event, "from_date": self.from_date, "to_date": self.to_date, "property_expr": self.property_expr, "unit": self.unit, "results": self.results, } ``` ## Bookmark Types ## mixpanel_headless.BookmarkInfo ``` BookmarkInfo( id: int, name: str, type: BookmarkType, project_id: int, created: str, modified: str, workspace_id: int | None = None, dashboard_id: int | None = None, description: str | None = None, creator_id: int | None = None, creator_name: str | None = None, ) ``` Metadata for a saved report (bookmark) from the Mixpanel Bookmarks API. Represents a saved Insights, Funnel, Retention, or Flows report that can be queried using query_saved_report() or query_saved_flows(). | ATTRIBUTE | DESCRIPTION | | -------------- | -------------------------------------------------------------------------------------------- | | `id` | Unique bookmark identifier. **TYPE:** `int` | | `name` | User-defined report name. **TYPE:** `str` | | `type` | Report type (insights, funnels, retention, flows, launch-analysis). **TYPE:** `BookmarkType` | | `project_id` | Parent Mixpanel project ID. **TYPE:** `int` | | `created` | Creation timestamp (ISO format). **TYPE:** `str` | | `modified` | Last modification timestamp (ISO format). **TYPE:** `str` | | `workspace_id` | Optional workspace ID if scoped to a workspace. **TYPE:** \`int | | `dashboard_id` | Optional parent dashboard ID if linked to a dashboard. **TYPE:** \`int | | `description` | Optional user-provided description. **TYPE:** \`str | | `creator_id` | Optional creator's user ID. **TYPE:** \`int | | `creator_name` | Optional creator's display name. **TYPE:** \`str | ### id ``` id: int ``` Unique bookmark identifier. ### name ``` name: str ``` User-defined report name. ### type ``` type: BookmarkType ``` Report type. ### project_id ``` project_id: int ``` Parent Mixpanel project ID. ### created ``` created: str ``` Creation timestamp (ISO format). ### modified ``` modified: str ``` Last modification timestamp (ISO format). ### workspace_id ``` workspace_id: int | None = None ``` Workspace ID if scoped to a workspace. ### dashboard_id ``` dashboard_id: int | None = None ``` Parent dashboard ID if linked to a dashboard. ### description ``` description: str | None = None ``` User-provided description. ### creator_id ``` creator_id: int | None = None ``` Creator's user ID. ### creator_name ``` creator_name: str | None = None ``` Creator's display name. ### to_dict ``` to_dict() -> dict[str, Any] ``` Serialize for JSON output. | RETURNS | DESCRIPTION | | ---------------- | --------------------------------------------- | | `dict[str, Any]` | Dictionary with all bookmark metadata fields. | Source code in `src/mixpanel_headless/types.py` ``` def to_dict(self) -> dict[str, Any]: """Serialize for JSON output. Returns: Dictionary with all bookmark metadata fields. """ result: dict[str, Any] = { "id": self.id, "name": self.name, "type": self.type, "project_id": self.project_id, "created": self.created, "modified": self.modified, } if self.workspace_id is not None: result["workspace_id"] = self.workspace_id if self.dashboard_id is not None: result["dashboard_id"] = self.dashboard_id if self.description is not None: result["description"] = self.description if self.creator_id is not None: result["creator_id"] = self.creator_id if self.creator_name is not None: result["creator_name"] = self.creator_name return result ``` ## mixpanel_headless.SavedReportResult ``` SavedReportResult( bookmark_id: int, computed_at: str, from_date: str, to_date: str, headers: list[str] = list(), series: dict[str, Any] = dict(), _df_cache: DataFrame | None = None, ) ``` Data from a saved report (Insights, Retention, or Funnel). Contains data from a pre-configured saved report with automatic report type detection and lazy DataFrame conversion support. The report_type property automatically detects the report type based on headers: "$retention" indicates retention, "$funnel" indicates funnel, otherwise it's an insights report. | ATTRIBUTE | DESCRIPTION | | ------------- | ------------------------------------------------------------------------- | | `bookmark_id` | Saved report identifier. **TYPE:** `int` | | `computed_at` | When report was computed (ISO format). **TYPE:** `str` | | `from_date` | Report start date. **TYPE:** `str` | | `to_date` | Report end date. **TYPE:** `str` | | `headers` | Report column headers (used for type detection). **TYPE:** `list[str]` | | `series` | Report data (structure varies by report type). **TYPE:** `dict[str, Any]` | ### bookmark_id ``` bookmark_id: int ``` Saved report identifier. ### computed_at ``` computed_at: str ``` When report was computed (ISO format). ### from_date ``` from_date: str ``` Report start date. ### to_date ``` to_date: str ``` Report end date. ### headers ``` headers: list[str] = field(default_factory=list) ``` Report column headers (used for type detection). ### series ``` series: dict[str, Any] = field(default_factory=dict) ``` Report data (structure varies by report type). For Insights reports: {event_name: {date: count}} For Retention reports: {series_name: {date: {segment: {first, counts, rates}}}} For Funnel reports: {count: {...}, overall_conv_ratio: {...}, ...} ### report_type ``` report_type: SavedReportType ``` Detect the report type from headers. | RETURNS | DESCRIPTION | | ----------------- | -------------------------------------------- | | `SavedReportType` | 'retention' if headers contain '$retention', | | `SavedReportType` | 'funnel' if headers contain '$funnel', | | `SavedReportType` | 'flows' if headers contain '$flows', | | `SavedReportType` | 'insights' otherwise. | ### df ``` df: DataFrame ``` Convert to DataFrame. For Insights reports: columns are date, event, count. For Retention/Funnel reports: flattens the nested structure. Conversion is lazy - computed on first access and cached. ### to_dict ``` to_dict() -> dict[str, Any] ``` Serialize for JSON output. | RETURNS | DESCRIPTION | | ---------------- | ----------------------------------------------------------------- | | `dict[str, Any]` | Dictionary with all report fields including detected report_type. | Source code in `src/mixpanel_headless/types.py` ``` def to_dict(self) -> dict[str, Any]: """Serialize for JSON output. Returns: Dictionary with all report fields including detected report_type. """ return { "bookmark_id": self.bookmark_id, "computed_at": self.computed_at, "from_date": self.from_date, "to_date": self.to_date, "headers": self.headers, "series": self.series, "report_type": self.report_type, } ``` ## mixpanel_headless.FlowsResult ``` FlowsResult( bookmark_id: int, computed_at: str, steps: list[dict[str, Any]] = list(), breakdowns: list[dict[str, Any]] = list(), overall_conversion_rate: float = 0.0, metadata: dict[str, Any] = dict(), *, _df_cache: DataFrame | None = None, ) ``` Bases: `ResultWithDataFrame` Data from a saved Flows report. Contains user path/navigation data from a pre-configured Flows report with lazy DataFrame conversion support. Inherits from ResultWithDataFrame to provide: - Lazy DataFrame caching via \_df_cache field - Normalized table output via to_table_dict() method | ATTRIBUTE | DESCRIPTION | | ------------------------- | ------------------------------------------------------------------------------------ | | `bookmark_id` | Saved report identifier. **TYPE:** `int` | | `computed_at` | When report was computed (ISO format). **TYPE:** `str` | | `steps` | Flow step data with event sequences and counts. **TYPE:** `list[dict[str, Any]]` | | `breakdowns` | Path breakdown data showing user flow distribution. **TYPE:** `list[dict[str, Any]]` | | `overall_conversion_rate` | End-to-end conversion rate (0.0 to 1.0). **TYPE:** `float` | | `metadata` | Additional API metadata from the response. **TYPE:** `dict[str, Any]` | ### bookmark_id ``` bookmark_id: int ``` Saved report identifier. ### computed_at ``` computed_at: str ``` When report was computed (ISO format). ### steps ``` steps: list[dict[str, Any]] = field(default_factory=list) ``` Flow step data with event sequences and counts. ### breakdowns ``` breakdowns: list[dict[str, Any]] = field(default_factory=list) ``` Path breakdown data showing user flow distribution. ### overall_conversion_rate ``` overall_conversion_rate: float = 0.0 ``` End-to-end conversion rate (0.0 to 1.0). ### metadata ``` metadata: dict[str, Any] = field(default_factory=dict) ``` Additional API metadata from the response. ### df ``` df: DataFrame ``` Convert steps to DataFrame. Returns DataFrame with columns derived from step data structure. Conversion is lazy - computed on first access and cached. ### to_dict ``` to_dict() -> dict[str, Any] ``` Serialize for JSON output. | RETURNS | DESCRIPTION | | ---------------- | ---------------------------------------- | | `dict[str, Any]` | Dictionary with all flows report fields. | Source code in `src/mixpanel_headless/types.py` ``` def to_dict(self) -> dict[str, Any]: """Serialize for JSON output. Returns: Dictionary with all flows report fields. """ return { "bookmark_id": self.bookmark_id, "computed_at": self.computed_at, "steps": self.steps, "breakdowns": self.breakdowns, "overall_conversion_rate": self.overall_conversion_rate, "metadata": self.metadata, } ``` ## JQL Discovery Types ## mixpanel_headless.PropertyDistributionResult ``` PropertyDistributionResult( event: str, property_name: str, from_date: str, to_date: str, total_count: int, values: tuple[PropertyValueCount, ...], _df_cache: DataFrame | None = None, ) ``` Distribution of values for a property from JQL analysis. Contains the top N values for a property with their counts and percentages, enabling quick understanding of property value distribution without processing all raw events. | ATTRIBUTE | DESCRIPTION | | --------------- | ---------------------------------------------------------------------------------- | | `event` | The event type analyzed. **TYPE:** `str` | | `property_name` | The property name analyzed. **TYPE:** `str` | | `from_date` | Query start date (YYYY-MM-DD). **TYPE:** `str` | | `to_date` | Query end date (YYYY-MM-DD). **TYPE:** `str` | | `total_count` | Total number of events with this property defined. **TYPE:** `int` | | `values` | Top values with counts and percentages. **TYPE:** `tuple[PropertyValueCount, ...]` | ### event ``` event: str ``` Event type analyzed. ### property_name ``` property_name: str ``` Property name analyzed. ### from_date ``` from_date: str ``` Query start date (YYYY-MM-DD). ### to_date ``` to_date: str ``` Query end date (YYYY-MM-DD). ### total_count ``` total_count: int ``` Total events with this property defined. ### values ``` values: tuple[PropertyValueCount, ...] ``` Top values with counts and percentages. ### df ``` df: DataFrame ``` Convert to DataFrame with columns: value, count, percentage. Conversion is lazy - computed on first access and cached. | RETURNS | DESCRIPTION | | ----------- | --------------------------------------- | | `DataFrame` | DataFrame with value distribution data. | ### to_dict ``` to_dict() -> dict[str, Any] ``` Serialize for JSON output. | RETURNS | DESCRIPTION | | ---------------- | -------------------------------------- | | `dict[str, Any]` | Dictionary with all distribution data. | Source code in `src/mixpanel_headless/types.py` ``` def to_dict(self) -> dict[str, Any]: """Serialize for JSON output. Returns: Dictionary with all distribution data. """ return { "event": self.event, "property_name": self.property_name, "from_date": self.from_date, "to_date": self.to_date, "total_count": self.total_count, "values": [v.to_dict() for v in self.values], } ``` ## mixpanel_headless.PropertyValueCount ``` PropertyValueCount( value: str | int | float | bool | None, count: int, percentage: float ) ``` A single value and its count from property distribution analysis. Represents one row in a property value distribution, showing the value, its occurrence count, and percentage of total. | ATTRIBUTE | DESCRIPTION | | ------------ | -------------------------------------------------------------------------- | | `value` | The property value (can be string, number, bool, or None). **TYPE:** \`str | | `count` | Number of occurrences of this value. **TYPE:** `int` | | `percentage` | Percentage of total events (0.0 to 100.0). **TYPE:** `float` | ### value ``` value: str | int | float | bool | None ``` The property value. ### count ``` count: int ``` Number of occurrences. ### percentage ``` percentage: float ``` Percentage of total (0.0 to 100.0). ### to_dict ``` to_dict() -> dict[str, Any] ``` Serialize for JSON output. | RETURNS | DESCRIPTION | | ---------------- | --------------------------------------------- | | `dict[str, Any]` | Dictionary with value, count, and percentage. | Source code in `src/mixpanel_headless/types.py` ``` def to_dict(self) -> dict[str, Any]: """Serialize for JSON output. Returns: Dictionary with value, count, and percentage. """ return { "value": self.value, "count": self.count, "percentage": self.percentage, } ``` ## mixpanel_headless.NumericPropertySummaryResult ``` NumericPropertySummaryResult( event: str, property_name: str, from_date: str, to_date: str, count: int, min: float, max: float, sum: float, avg: float, stddev: float, percentiles: dict[int, float], ) ``` Statistical summary of a numeric property from JQL analysis. Contains min, max, sum, average, standard deviation, and percentiles for a numeric property, enabling understanding of value distributions without processing all raw events. | ATTRIBUTE | DESCRIPTION | | --------------- | -------------------------------------------------------------------------- | | `event` | The event type analyzed. **TYPE:** `str` | | `property_name` | The property name analyzed. **TYPE:** `str` | | `from_date` | Query start date (YYYY-MM-DD). **TYPE:** `str` | | `to_date` | Query end date (YYYY-MM-DD). **TYPE:** `str` | | `count` | Number of events with this property defined. **TYPE:** `int` | | `min` | Minimum value. **TYPE:** `float` | | `max` | Maximum value. **TYPE:** `float` | | `sum` | Sum of all values. **TYPE:** `float` | | `avg` | Average value. **TYPE:** `float` | | `stddev` | Standard deviation. **TYPE:** `float` | | `percentiles` | Percentile values keyed by percentile number. **TYPE:** `dict[int, float]` | ### event ``` event: str ``` Event type analyzed. ### property_name ``` property_name: str ``` Property name analyzed. ### from_date ``` from_date: str ``` Query start date (YYYY-MM-DD). ### to_date ``` to_date: str ``` Query end date (YYYY-MM-DD). ### count ``` count: int ``` Number of events with this property defined. ### min ``` min: float ``` Minimum value. ### max ``` max: float ``` Maximum value. ### sum ``` sum: float ``` Sum of all values. ### avg ``` avg: float ``` Average value. ### stddev ``` stddev: float ``` Standard deviation. ### percentiles ``` percentiles: dict[int, float] ``` Percentile values keyed by percentile number (e.g., {50: 98.0}). ### to_dict ``` to_dict() -> dict[str, Any] ``` Serialize for JSON output. | RETURNS | DESCRIPTION | | ---------------- | ----------------------------------------- | | `dict[str, Any]` | Dictionary with all numeric summary data. | Source code in `src/mixpanel_headless/types.py` ``` def to_dict(self) -> dict[str, Any]: """Serialize for JSON output. Returns: Dictionary with all numeric summary data. """ return { "event": self.event, "property_name": self.property_name, "from_date": self.from_date, "to_date": self.to_date, "count": self.count, "min": self.min, "max": self.max, "sum": self.sum, "avg": self.avg, "stddev": self.stddev, "percentiles": {str(k): v for k, v in self.percentiles.items()}, } ``` ## mixpanel_headless.DailyCountsResult ``` DailyCountsResult( from_date: str, to_date: str, events: tuple[str, ...] | None, counts: tuple[DailyCount, ...], _df_cache: DataFrame | None = None, ) ``` Time-series event counts by day from JQL analysis. Contains daily event counts for quick activity trend analysis without complex segmentation setup. | ATTRIBUTE | DESCRIPTION | | ----------- | ----------------------------------------------------------------------- | | `from_date` | Query start date (YYYY-MM-DD). **TYPE:** `str` | | `to_date` | Query end date (YYYY-MM-DD). **TYPE:** `str` | | `events` | Event types included (None for all events). **TYPE:** \`tuple[str, ...] | | `counts` | Daily counts for each event. **TYPE:** `tuple[DailyCount, ...]` | ### from_date ``` from_date: str ``` Query start date (YYYY-MM-DD). ### to_date ``` to_date: str ``` Query end date (YYYY-MM-DD). ### events ``` events: tuple[str, ...] | None ``` Event types included (None for all events). ### counts ``` counts: tuple[DailyCount, ...] ``` Daily counts for each event. ### df ``` df: DataFrame ``` Convert to DataFrame with columns: date, event, count. Conversion is lazy - computed on first access and cached. | RETURNS | DESCRIPTION | | ----------- | --------------------------------- | | `DataFrame` | DataFrame with daily counts data. | ### to_dict ``` to_dict() -> dict[str, Any] ``` Serialize for JSON output. | RETURNS | DESCRIPTION | | ---------------- | -------------------------------------- | | `dict[str, Any]` | Dictionary with all daily counts data. | Source code in `src/mixpanel_headless/types.py` ``` def to_dict(self) -> dict[str, Any]: """Serialize for JSON output. Returns: Dictionary with all daily counts data. """ return { "from_date": self.from_date, "to_date": self.to_date, "events": list(self.events) if self.events else None, "counts": [c.to_dict() for c in self.counts], } ``` ## mixpanel_headless.DailyCount ``` DailyCount(date: str, event: str, count: int) ``` Event count for a single date from daily counts analysis. Represents one row in a daily counts result, showing date, event, and count. | ATTRIBUTE | DESCRIPTION | | --------- | --------------------------------------------------- | | `date` | Date string (YYYY-MM-DD). **TYPE:** `str` | | `event` | Event name. **TYPE:** `str` | | `count` | Number of occurrences on this date. **TYPE:** `int` | ### date ``` date: str ``` Date string (YYYY-MM-DD). ### event ``` event: str ``` Event name. ### count ``` count: int ``` Number of occurrences. ### to_dict ``` to_dict() -> dict[str, Any] ``` Serialize for JSON output. | RETURNS | DESCRIPTION | | ---------------- | --------------------------------------- | | `dict[str, Any]` | Dictionary with date, event, and count. | Source code in `src/mixpanel_headless/types.py` ``` def to_dict(self) -> dict[str, Any]: """Serialize for JSON output. Returns: Dictionary with date, event, and count. """ return { "date": self.date, "event": self.event, "count": self.count, } ``` ## mixpanel_headless.EngagementDistributionResult ``` EngagementDistributionResult( from_date: str, to_date: str, events: tuple[str, ...] | None, total_users: int, buckets: tuple[EngagementBucket, ...], _df_cache: DataFrame | None = None, ) ``` User engagement distribution from JQL analysis. Shows how many users performed N events, helping understand user engagement patterns without processing all raw events. | ATTRIBUTE | DESCRIPTION | | ------------- | ----------------------------------------------------------------------------- | | `from_date` | Query start date (YYYY-MM-DD). **TYPE:** `str` | | `to_date` | Query end date (YYYY-MM-DD). **TYPE:** `str` | | `events` | Event types included (None for all events). **TYPE:** \`tuple[str, ...] | | `total_users` | Total number of distinct users. **TYPE:** `int` | | `buckets` | Engagement buckets with user counts. **TYPE:** `tuple[EngagementBucket, ...]` | ### from_date ``` from_date: str ``` Query start date (YYYY-MM-DD). ### to_date ``` to_date: str ``` Query end date (YYYY-MM-DD). ### events ``` events: tuple[str, ...] | None ``` Event types included (None for all events). ### total_users ``` total_users: int ``` Total number of distinct users. ### buckets ``` buckets: tuple[EngagementBucket, ...] ``` Engagement buckets with user counts. ### df ``` df: DataFrame ``` Convert to DataFrame with engagement bucket columns. Conversion is lazy - computed on first access and cached. | RETURNS | DESCRIPTION | | ----------- | -------------------------------------------- | | `DataFrame` | DataFrame with engagement distribution data. | ### to_dict ``` to_dict() -> dict[str, Any] ``` Serialize for JSON output. | RETURNS | DESCRIPTION | | ---------------- | ------------------------------------------------- | | `dict[str, Any]` | Dictionary with all engagement distribution data. | Source code in `src/mixpanel_headless/types.py` ``` def to_dict(self) -> dict[str, Any]: """Serialize for JSON output. Returns: Dictionary with all engagement distribution data. """ return { "from_date": self.from_date, "to_date": self.to_date, "events": list(self.events) if self.events else None, "total_users": self.total_users, "buckets": [b.to_dict() for b in self.buckets], } ``` ## mixpanel_headless.EngagementBucket ``` EngagementBucket( bucket_min: int, bucket_label: str, user_count: int, percentage: float ) ``` User count in an engagement bucket from engagement analysis. Represents one bucket in a user engagement distribution, showing how many users performed events in a certain frequency range. | ATTRIBUTE | DESCRIPTION | | -------------- | ---------------------------------------------------------------- | | `bucket_min` | Minimum events in this bucket. **TYPE:** `int` | | `bucket_label` | Human-readable label (e.g., "1", "2-5", "100+"). **TYPE:** `str` | | `user_count` | Number of users in this bucket. **TYPE:** `int` | | `percentage` | Percentage of total users (0.0 to 100.0). **TYPE:** `float` | ### bucket_min ``` bucket_min: int ``` Minimum events in this bucket. ### bucket_label ``` bucket_label: str ``` Human-readable label (e.g., '1', '2-5', '100+'). ### user_count ``` user_count: int ``` Number of users in this bucket. ### percentage ``` percentage: float ``` Percentage of total users (0.0 to 100.0). ### to_dict ``` to_dict() -> dict[str, Any] ``` Serialize for JSON output. | RETURNS | DESCRIPTION | | ---------------- | ---------------------------- | | `dict[str, Any]` | Dictionary with bucket data. | Source code in `src/mixpanel_headless/types.py` ``` def to_dict(self) -> dict[str, Any]: """Serialize for JSON output. Returns: Dictionary with bucket data. """ return { "bucket_min": self.bucket_min, "bucket_label": self.bucket_label, "user_count": self.user_count, "percentage": self.percentage, } ``` ## mixpanel_headless.PropertyCoverageResult ``` PropertyCoverageResult( event: str, from_date: str, to_date: str, total_events: int, coverage: tuple[PropertyCoverage, ...], _df_cache: DataFrame | None = None, ) ``` Property coverage analysis result from JQL. Shows which properties are consistently populated vs sparse, helping understand data quality before writing queries. | ATTRIBUTE | DESCRIPTION | | -------------- | ------------------------------------------------------------------------------- | | `event` | The event type analyzed. **TYPE:** `str` | | `from_date` | Query start date (YYYY-MM-DD). **TYPE:** `str` | | `to_date` | Query end date (YYYY-MM-DD). **TYPE:** `str` | | `total_events` | Total number of events analyzed. **TYPE:** `int` | | `coverage` | Coverage statistics for each property. **TYPE:** `tuple[PropertyCoverage, ...]` | ### event ``` event: str ``` Event type analyzed. ### from_date ``` from_date: str ``` Query start date (YYYY-MM-DD). ### to_date ``` to_date: str ``` Query end date (YYYY-MM-DD). ### total_events ``` total_events: int ``` Total number of events analyzed. ### coverage ``` coverage: tuple[PropertyCoverage, ...] ``` Coverage statistics for each property. ### df ``` df: DataFrame ``` Convert to DataFrame with property coverage columns. Conversion is lazy - computed on first access and cached. | RETURNS | DESCRIPTION | | ----------- | -------------------------------------- | | `DataFrame` | DataFrame with property coverage data. | ### to_dict ``` to_dict() -> dict[str, Any] ``` Serialize for JSON output. | RETURNS | DESCRIPTION | | ---------------- | ---------------------------------- | | `dict[str, Any]` | Dictionary with all coverage data. | Source code in `src/mixpanel_headless/types.py` ``` def to_dict(self) -> dict[str, Any]: """Serialize for JSON output. Returns: Dictionary with all coverage data. """ return { "event": self.event, "from_date": self.from_date, "to_date": self.to_date, "total_events": self.total_events, "coverage": [c.to_dict() for c in self.coverage], } ``` ## mixpanel_headless.PropertyCoverage ``` PropertyCoverage( property: str, defined_count: int, null_count: int, coverage_percentage: float, ) ``` Coverage statistics for a single property from coverage analysis. Shows how often a property is defined vs null for a given event type. | ATTRIBUTE | DESCRIPTION | | --------------------- | ------------------------------------------------------------------------- | | `property` | Property name. **TYPE:** `str` | | `defined_count` | Number of events with this property defined. **TYPE:** `int` | | `null_count` | Number of events with this property null/undefined. **TYPE:** `int` | | `coverage_percentage` | Percentage of events with property defined (0.0-100.0). **TYPE:** `float` | ### property ``` property: str ``` Property name. ### defined_count ``` defined_count: int ``` Number of events with property defined. ### null_count ``` null_count: int ``` Number of events with property null/undefined. ### coverage_percentage ``` coverage_percentage: float ``` Percentage with property defined (0.0 to 100.0). ### to_dict ``` to_dict() -> dict[str, Any] ``` Serialize for JSON output. | RETURNS | DESCRIPTION | | ---------------- | ------------------------------ | | `dict[str, Any]` | Dictionary with coverage data. | Source code in `src/mixpanel_headless/types.py` ``` def to_dict(self) -> dict[str, Any]: """Serialize for JSON output. Returns: Dictionary with coverage data. """ return { "property": self.property, "defined_count": self.defined_count, "null_count": self.null_count, "coverage_percentage": self.coverage_percentage, } ``` ## Dashboard CRUD Types ## mixpanel_headless.Dashboard Bases: `BaseModel` A Mixpanel dashboard as returned by the App API. Represents the full dashboard entity including metadata, permissions, and optional layout/content fields. Extra fields from API evolution are preserved via `extra="allow"`. | ATTRIBUTE | DESCRIPTION | | ------------------------- | ----------------------------------------------------------------------- | | `id` | Unique dashboard identifier. **TYPE:** `int` | | `title` | Dashboard title. **TYPE:** `str` | | `description` | Dashboard description. **TYPE:** \`str | | `is_private` | Whether the dashboard is private. **TYPE:** `bool` | | `is_restricted` | Whether the dashboard has restricted access. **TYPE:** `bool` | | `creator_id` | ID of the dashboard creator. **TYPE:** \`int | | `creator_name` | Name of the dashboard creator. **TYPE:** \`str | | `creator_email` | Email of the dashboard creator. **TYPE:** \`str | | `created` | Creation timestamp (lenient parsing). **TYPE:** \`datetime | | `modified` | Last modification timestamp. **TYPE:** \`datetime | | `is_favorited` | Whether the current user has favorited this dashboard. **TYPE:** `bool` | | `pinned_date` | Date the dashboard was pinned, if any. **TYPE:** \`str | | `layout_version` | Layout version metadata. **TYPE:** \`Any | | `unique_view_count` | Number of unique viewers. **TYPE:** \`int | | `total_view_count` | Total view count. **TYPE:** \`int | | `last_modified_by_id` | ID of the last modifier. **TYPE:** \`int | | `last_modified_by_name` | Name of the last modifier. **TYPE:** \`str | | `last_modified_by_email` | Email of the last modifier. **TYPE:** \`str | | `filters` | Dashboard-level filters. **TYPE:** \`list[Any] | | `breakdowns` | Dashboard-level breakdowns. **TYPE:** \`list[Any] | | `time_filter` | Dashboard-level time filter. **TYPE:** \`Any | | `generation_type` | How the dashboard was generated. **TYPE:** \`str | | `parent_dashboard_id` | Parent dashboard ID for nested dashboards. **TYPE:** \`int | | `child_dashboards` | Child dashboard references. **TYPE:** \`list[Any] | | `can_update_basic` | Permission flag. **TYPE:** `bool` | | `can_share` | Permission flag. **TYPE:** `bool` | | `can_view` | Permission flag. **TYPE:** `bool` | | `can_update_restricted` | Permission flag. **TYPE:** `bool` | | `can_update_visibility` | Permission flag. **TYPE:** `bool` | | `is_superadmin` | Whether current user is superadmin. **TYPE:** `bool` | | `allow_staff_override` | Whether staff override is allowed. **TYPE:** `bool` | | `can_pin` | Whether current user can pin. **TYPE:** `bool` | | `is_shared_with_project` | Whether shared with the project. **TYPE:** `bool` | | `creator` | Creator identifier string. **TYPE:** \`str | | `ancestors` | Ancestor dashboard references. **TYPE:** `list[Any]` | | `layout` | Dashboard layout data. **TYPE:** \`Any | | `contents` | Dashboard contents data. **TYPE:** \`Any | | `num_active_public_links` | Number of active public links. **TYPE:** \`int | | `new_content` | New content data. **TYPE:** \`Any | | `template_type` | Template type if created from a template. **TYPE:** \`str | Example ``` dashboard = Dashboard( id=1, title="Q1 Metrics", is_private=False, is_restricted=False, is_favorited=False, can_update_basic=True, can_share=True, can_view=True, can_update_restricted=False, can_update_visibility=False, is_superadmin=False, allow_staff_override=False, can_pin=True, is_shared_with_project=True, ancestors=[], ) assert dashboard.title == "Q1 Metrics" ``` ### id ``` id: int ``` Unique dashboard identifier. ### title ``` title: str ``` Dashboard title. ### description ``` description: str | None = None ``` Dashboard description. ### is_private ``` is_private: bool = False ``` Whether the dashboard is private. ### is_restricted ``` is_restricted: bool = False ``` Whether the dashboard has restricted access. ### creator_id ``` creator_id: int | None = None ``` ID of the dashboard creator. ### creator_name ``` creator_name: str | None = None ``` Name of the dashboard creator. ### creator_email ``` creator_email: str | None = None ``` Email of the dashboard creator. ### created ``` created: datetime | None = None ``` Creation timestamp. ### modified ``` modified: datetime | None = None ``` Last modification timestamp. ### is_favorited ``` is_favorited: bool = False ``` Whether the current user has favorited this dashboard. ### pinned_date ``` pinned_date: str | None = None ``` Date the dashboard was pinned, if any. ### layout_version ``` layout_version: Any | None = None ``` Layout version metadata. ### unique_view_count ``` unique_view_count: int | None = None ``` Number of unique viewers. ### total_view_count ``` total_view_count: int | None = None ``` Total view count. ### last_modified_by_id ``` last_modified_by_id: int | None = None ``` ID of the last modifier. ### last_modified_by_name ``` last_modified_by_name: str | None = None ``` Name of the last modifier. ### last_modified_by_email ``` last_modified_by_email: str | None = None ``` Email of the last modifier. ### filters ``` filters: list[Any] | None = None ``` Dashboard-level filters. ### breakdowns ``` breakdowns: list[Any] | None = None ``` Dashboard-level breakdowns. ### time_filter ``` time_filter: Any | None = None ``` Dashboard-level time filter. ### generation_type ``` generation_type: str | None = None ``` How the dashboard was generated. ### parent_dashboard_id ``` parent_dashboard_id: int | None = None ``` Parent dashboard ID for nested dashboards. ### child_dashboards ``` child_dashboards: list[Any] | None = None ``` Child dashboard references. ### can_update_basic ``` can_update_basic: bool = False ``` Permission: can update basic fields. ### can_share ``` can_share: bool = False ``` Permission: can share. ### can_view ``` can_view: bool = False ``` Permission: can view. ### can_update_restricted ``` can_update_restricted: bool = False ``` Permission: can update restricted fields. ### can_update_visibility ``` can_update_visibility: bool = False ``` Permission: can update visibility. ### is_superadmin ``` is_superadmin: bool = False ``` Whether current user is superadmin. ### allow_staff_override ``` allow_staff_override: bool = False ``` Whether staff override is allowed. ### can_pin ``` can_pin: bool = False ``` Whether current user can pin. ### is_shared_with_project ``` is_shared_with_project: bool = False ``` Whether shared with the project. ### creator ``` creator: str | None = None ``` Creator identifier string. ### ancestors ``` ancestors: list[Any] = Field(default_factory=list) ``` Ancestor dashboard references. ### layout ``` layout: Any | None = None ``` Dashboard layout data. ### contents ``` contents: Any | None = None ``` Dashboard contents data. ### num_active_public_links ``` num_active_public_links: int | None = None ``` Number of active public links. ### new_content ``` new_content: Any | None = None ``` New content data. ### template_type ``` template_type: str | None = None ``` Template type if created from a template. ## mixpanel_headless.CreateDashboardParams Bases: `BaseModel` Parameters for creating a new dashboard. | ATTRIBUTE | DESCRIPTION | | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `title` | Dashboard title (required). **TYPE:** `str` | | `description` | Dashboard description. **TYPE:** \`str | | `is_private` | Whether the dashboard should be private. **TYPE:** \`bool | | `is_restricted` | Whether the dashboard should have restricted access. **TYPE:** \`bool | | `filters` | Dashboard-level filters. **TYPE:** \`list[Any] | | `breakdowns` | Dashboard-level breakdowns. **TYPE:** \`list[Any] | | `time_filter` | Dashboard-level time filter. **TYPE:** \`Any | | `duplicate` | ID of dashboard to duplicate. **TYPE:** \`int | | `rows` | Initial dashboard content with layout. Each row contains 1-4 content items (text cards or reports). Items in the same row are placed side-by-side with auto-distributed widths. This is the recommended way to create dashboards with proper layout β€” adding content after creation via update_dashboard() places each item in its own full-width row, and layout restructuring (merging items into shared rows) is not supported via PATCH. **TYPE:** \`list[DashboardRow] | Example ``` import json params = CreateDashboardParams( title="Product Health", rows=[ DashboardRow(contents=[ DashboardRowContent( content_type="text", content_params={"markdown": "

Overview

"}, ), ]), DashboardRow(contents=[ DashboardRowContent( content_type="report", content_params={"bookmark": { "name": "DAU", "type": "insights", "params": json.dumps(dau_result.params), }}, ), DashboardRowContent( content_type="report", content_params={"bookmark": { "name": "Signups", "type": "insights", "params": json.dumps(signup_result.params), }}, ), ]), ], ) ``` ### title ``` title: str ``` Dashboard title (required). ### description ``` description: str | None = None ``` Dashboard description. ### is_private ``` is_private: bool | None = None ``` Whether the dashboard should be private. ### is_restricted ``` is_restricted: bool | None = None ``` Whether the dashboard should have restricted access. ### filters ``` filters: list[Any] | None = None ``` Dashboard-level filters. ### breakdowns ``` breakdowns: list[Any] | None = None ``` Dashboard-level breakdowns. ### time_filter ``` time_filter: Any | None = None ``` Dashboard-level time filter. ### duplicate ``` duplicate: int | None = None ``` ID of dashboard to duplicate. ### rows ``` rows: list[DashboardRow] | None = None ``` Initial content rows with layout. Each row has 1-4 content items. ## mixpanel_headless.UpdateDashboardParams Bases: `BaseModel` Parameters for updating an existing dashboard. All fields are optional β€” only provided fields are sent to the API. | ATTRIBUTE | DESCRIPTION | | --------------- | ----------------------------------------------------- | | `title` | New dashboard title. **TYPE:** \`str | | `description` | New dashboard description. **TYPE:** \`str | | `is_private` | New privacy setting. **TYPE:** \`bool | | `is_restricted` | New restriction setting. **TYPE:** \`bool | | `filters` | New dashboard-level filters. **TYPE:** \`list[Any] | | `breakdowns` | New dashboard-level breakdowns. **TYPE:** \`list[Any] | | `time_filter` | New dashboard-level time filter. **TYPE:** \`Any | | `layout` | New dashboard layout data. **TYPE:** \`Any | | `content` | New dashboard content data. **TYPE:** \`Any | Example ``` params = UpdateDashboardParams(title="Q1 Metrics v2") data = params.model_dump(exclude_none=True) # {"title": "Q1 Metrics v2"} ``` ### title ``` title: str | None = None ``` New dashboard title. ### description ``` description: str | None = None ``` New dashboard description. ### is_private ``` is_private: bool | None = None ``` New privacy setting. ### is_restricted ``` is_restricted: bool | None = None ``` New restriction setting. ### filters ``` filters: list[Any] | None = None ``` New dashboard-level filters. ### breakdowns ``` breakdowns: list[Any] | None = None ``` New dashboard-level breakdowns. ### time_filter ``` time_filter: Any | None = None ``` New dashboard-level time filter. ### layout ``` layout: Any | None = None ``` New dashboard layout data. ### content ``` content: Any | None = None ``` New dashboard content data. ## mixpanel_headless.BlueprintTemplate Bases: `BaseModel` A dashboard blueprint template. | ATTRIBUTE | DESCRIPTION | | ----------------------------- | -------------------------------------------------- | | `title_key` | Template title key. **TYPE:** `str` | | `description_key` | Template description key. **TYPE:** `str` | | `alternative_description_key` | Alternative description key. **TYPE:** \`str | | `number_of_reports` | Number of reports in the template. **TYPE:** \`int | Example ``` template = BlueprintTemplate( title_key="onboarding", description_key="Get started" ) ``` ### title_key ``` title_key: str ``` Template title key. ### description_key ``` description_key: str ``` Template description key. ### alternative_description_key ``` alternative_description_key: str | None = None ``` Alternative description key. ### number_of_reports ``` number_of_reports: int | None = None ``` Number of reports in the template. ## mixpanel_headless.BlueprintConfig Bases: `BaseModel` Configuration for a dashboard blueprint. | ATTRIBUTE | DESCRIPTION | | ----------- | ------------------------------------------------------ | | `variables` | Template variable mappings. **TYPE:** `dict[str, str]` | Example ``` config = BlueprintConfig(variables={"event": "Signup"}) ``` ### variables ``` variables: dict[str, str] ``` Template variable mappings. ## mixpanel_headless.BlueprintCard Bases: `BaseModel` A card in a blueprint dashboard. | ATTRIBUTE | DESCRIPTION | | -------------- | ------------------------------------------------- | | `card_type` | Card type (serialized as "type"). **TYPE:** `str` | | `text_card_id` | Text card ID, if applicable. **TYPE:** \`int | | `bookmark_id` | Bookmark ID, if applicable. **TYPE:** \`int | | `markdown` | Markdown content for text cards. **TYPE:** \`str | | `name` | Card name. **TYPE:** \`str | | `params` | Card parameters. **TYPE:** \`dict[str, Any] | Example ``` card = BlueprintCard(card_type="report", bookmark_id=123) data = card.model_dump(by_alias=True, exclude_none=True) # {"type": "report", "bookmark_id": 123} ``` ### card_type ``` card_type: str = Field(alias='type') ``` Card type (serialized as `"type"`). ### text_card_id ``` text_card_id: int | None = None ``` Text card ID, if applicable. ### bookmark_id ``` bookmark_id: int | None = None ``` Bookmark ID, if applicable. ### markdown ``` markdown: str | None = None ``` Markdown content for text cards. ### name ``` name: str | None = None ``` Card name. ### params ``` params: dict[str, Any] | None = None ``` Card parameters. ## mixpanel_headless.BlueprintFinishParams Bases: `BaseModel` Parameters for finalizing a blueprint dashboard. | ATTRIBUTE | DESCRIPTION | | -------------- | ---------------------------------------------------------- | | `dashboard_id` | ID of the blueprint dashboard to finalize. **TYPE:** `int` | | `cards` | List of cards to include. **TYPE:** `list[BlueprintCard]` | Example ``` params = BlueprintFinishParams( dashboard_id=1, cards=[BlueprintCard(card_type="report", bookmark_id=123)], ) ``` ### dashboard_id ``` dashboard_id: int ``` ID of the blueprint dashboard to finalize. ### cards ``` cards: list[BlueprintCard] ``` List of cards to include. ## mixpanel_headless.CreateRcaDashboardParams Bases: `BaseModel` Parameters for creating an RCA dashboard. | ATTRIBUTE | DESCRIPTION | | ----------------- | ---------------------------------------------------- | | `rca_source_id` | Source ID for RCA analysis. **TYPE:** `int` | | `rca_source_data` | Source data configuration. **TYPE:** `RcaSourceData` | Example ``` params = CreateRcaDashboardParams( rca_source_id=42, rca_source_data=RcaSourceData(source_type="anomaly"), ) ``` ### rca_source_id ``` rca_source_id: int ``` Source ID for RCA analysis. ### rca_source_data ``` rca_source_data: RcaSourceData ``` Source data configuration. ## mixpanel_headless.RcaSourceData Bases: `BaseModel` Source data for RCA dashboard creation. | ATTRIBUTE | DESCRIPTION | | --------------- | --------------------------------------------------- | | `source_type` | Source type (serialized as "type"). **TYPE:** `str` | | `date` | Date string. **TYPE:** \`str | | `metric_source` | Whether this is a metric source. **TYPE:** \`bool | Example ``` data = RcaSourceData(source_type="anomaly", date="2025-01-01") dumped = data.model_dump(by_alias=True, exclude_none=True) # {"type": "anomaly", "date": "2025-01-01"} ``` ### source_type ``` source_type: str = Field(alias='type') ``` Source type (serialized as `"type"`). ### date ``` date: str | None = None ``` Date string. ### metric_source ``` metric_source: bool | None = None ``` Whether this is a metric source. ## mixpanel_headless.UpdateReportLinkParams Bases: `BaseModel` Parameters for updating a report link on a dashboard. | ATTRIBUTE | DESCRIPTION | | ----------- | ------------------------------------------------- | | `link_type` | Link type (serialized as "type"). **TYPE:** `str` | Example ``` params = UpdateReportLinkParams(link_type="embedded") data = params.model_dump(by_alias=True, exclude_none=True) # {"type": "embedded"} ``` ### link_type ``` link_type: str = Field(alias='type') ``` Link type (serialized as `"type"`). ## mixpanel_headless.UpdateTextCardParams Bases: `BaseModel` Parameters for updating a text card on a dashboard. | ATTRIBUTE | DESCRIPTION | | ---------- | --------------------------------------------------- | | `markdown` | Markdown content for the text card. **TYPE:** \`str | Example ``` params = UpdateTextCardParams(markdown="# Hello") ``` ### markdown ``` markdown: str | None = None ``` Markdown content for the text card. ## Report CRUD Types ## mixpanel_headless.Bookmark Bases: `BaseModel` A Mixpanel bookmark (saved report) as returned by the App API. Represents the full bookmark entity including query parameters, metadata, and permissions. The `bookmark_type` field is aliased from `"type"` in the API response. | ATTRIBUTE | DESCRIPTION | | ---------------------------- | ----------------------------------------------------------------------------- | | `id` | Unique bookmark identifier. **TYPE:** `int` | | `project_id` | Parent project identifier. **TYPE:** \`int | | `name` | Bookmark name. **TYPE:** `str` | | `bookmark_type` | Report type (aliased from "type"). **TYPE:** `str` | | `description` | Bookmark description. **TYPE:** \`str | | `icon` | Bookmark icon. **TYPE:** \`str | | `params` | Query parameters (JSON value defining the report). **TYPE:** \`dict[str, Any] | | `dashboard_id` | Associated dashboard ID. **TYPE:** \`int | | `include_in_dashboard` | Whether included in dashboard. **TYPE:** \`bool | | `is_default` | Whether this is a default bookmark. **TYPE:** \`bool | | `creator_id` | ID of the creator. **TYPE:** \`int | | `creator_name` | Name of the creator. **TYPE:** \`str | | `creator_email` | Email of the creator. **TYPE:** \`str | | `created` | Creation timestamp. **TYPE:** \`datetime | | `modified` | Last modification timestamp. **TYPE:** \`datetime | | `last_modified_by_id` | ID of the last modifier. **TYPE:** \`int | | `last_modified_by_name` | Name of the last modifier. **TYPE:** \`str | | `last_modified_by_email` | Email of the last modifier. **TYPE:** \`str | | `metadata` | Report-specific metadata. **TYPE:** \`BookmarkMetadata | | `is_visibility_restricted` | Visibility restriction flag. **TYPE:** \`bool | | `is_modification_restricted` | Modification restriction flag. **TYPE:** \`bool | | `can_update_basic` | Permission flag. **TYPE:** \`bool | | `can_view` | Permission flag. **TYPE:** \`bool | | `can_share` | Permission flag. **TYPE:** \`bool | | `generation_type` | How the bookmark was generated. **TYPE:** \`str | | `original_type` | Original report type before conversion. **TYPE:** \`str | | `unique_view_count` | Number of unique viewers. **TYPE:** \`int | | `total_view_count` | Total view count. **TYPE:** \`int | Example ``` bookmark = Bookmark( id=1, name="Signup Funnel", bookmark_type="funnels", params={"events": [{"event": "Signup"}]}, ) assert bookmark.bookmark_type == "funnels" ``` ### id ``` id: int ``` Unique bookmark identifier. ### project_id ``` project_id: int | None = None ``` Parent project identifier. ### name ``` name: str ``` Bookmark name. ### bookmark_type ``` bookmark_type: str = Field(alias='type') ``` Report type (aliased from `"type"`). ### description ``` description: str | None = None ``` Bookmark description. ### icon ``` icon: str | None = None ``` Bookmark icon. ### params ``` params: dict[str, Any] | None = None ``` Query parameters (JSON value defining the report). ### dashboard_id ``` dashboard_id: int | None = None ``` Associated dashboard ID. ### include_in_dashboard ``` include_in_dashboard: bool | None = None ``` Whether included in dashboard. ### is_default ``` is_default: bool | None = None ``` Whether this is a default bookmark. ### creator_id ``` creator_id: int | None = None ``` ID of the creator. ### creator_name ``` creator_name: str | None = None ``` Name of the creator. ### creator_email ``` creator_email: str | None = None ``` Email of the creator. ### created ``` created: datetime | None = None ``` Creation timestamp. ### modified ``` modified: datetime | None = None ``` Last modification timestamp. ### last_modified_by_id ``` last_modified_by_id: int | None = None ``` ID of the last modifier. ### last_modified_by_name ``` last_modified_by_name: str | None = None ``` Name of the last modifier. ### last_modified_by_email ``` last_modified_by_email: str | None = None ``` Email of the last modifier. ### metadata ``` metadata: BookmarkMetadata | None = None ``` Report-specific metadata. ### is_visibility_restricted ``` is_visibility_restricted: bool | None = None ``` Visibility restriction flag. ### is_modification_restricted ``` is_modification_restricted: bool | None = None ``` Modification restriction flag. ### can_update_basic ``` can_update_basic: bool | None = None ``` Permission: can update basic fields. ### can_view ``` can_view: bool | None = None ``` Permission: can view. ### can_share ``` can_share: bool | None = None ``` Permission: can share. ### generation_type ``` generation_type: str | None = None ``` How the bookmark was generated. ### original_type ``` original_type: str | None = None ``` Original report type before conversion. ### unique_view_count ``` unique_view_count: int | None = None ``` Number of unique viewers. ### total_view_count ``` total_view_count: int | None = None ``` Total view count. ## mixpanel_headless.BookmarkMetadata Bases: `BaseModel` Metadata associated with a bookmark/report. Contains optional display and calculation settings that vary by bookmark type (insights, funnels, retention, etc.). | ATTRIBUTE | DESCRIPTION | | ---------------------------- | ------------------------------------------------- | | `table_display_mode` | Table display mode setting. **TYPE:** \`str | | `compare_enabled` | Whether comparison is enabled. **TYPE:** \`bool | | `compare_filters` | Comparison filter settings. **TYPE:** \`list[Any] | | `retention_calculation_type` | Retention calculation method. **TYPE:** \`str | | `event_name` | Associated event name. **TYPE:** \`str | | `funnel_conversion_window` | Funnel conversion window in days. **TYPE:** \`int | | `funnel_breakdown_limit` | Maximum funnel breakdown count. **TYPE:** \`int | Example ``` meta = BookmarkMetadata( table_display_mode="linear", compare_enabled=True, ) ``` ### table_display_mode ``` table_display_mode: str | None = None ``` Table display mode setting. ### compare_enabled ``` compare_enabled: bool | None = None ``` Whether comparison is enabled. ### compare_filters ``` compare_filters: list[Any] | None = None ``` Comparison filter settings. ### retention_calculation_type ``` retention_calculation_type: str | None = None ``` Retention calculation method. ### event_name ``` event_name: str | None = None ``` Associated event name. ### funnel_conversion_window ``` funnel_conversion_window: int | None = None ``` Funnel conversion window in days. ### funnel_breakdown_limit ``` funnel_breakdown_limit: int | None = None ``` Maximum funnel breakdown count. ## mixpanel_headless.CreateBookmarkParams Bases: `BaseModel` Parameters for creating a new bookmark/report. | ATTRIBUTE | DESCRIPTION | | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `name` | Bookmark name (required). **TYPE:** `str` | | `bookmark_type` | Report type (required, serialized as "type"). **TYPE:** `BookmarkTypeLiteral` | | `params` | Query parameters (required). **TYPE:** `dict[str, Any]` | | `description` | Bookmark description. **TYPE:** \`str | | `icon` | Bookmark icon. **TYPE:** \`str | | `dashboard_id` | Dashboard to associate with. Required by Workspace.create_bookmark() β€” the Mixpanel v2 API requires every bookmark to belong to a dashboard. **TYPE:** \`int | | `is_visibility_restricted` | Visibility restriction flag. **TYPE:** \`bool | | `is_modification_restricted` | Modification restriction flag. **TYPE:** \`bool | Example ``` params = CreateBookmarkParams( name="Signup Funnel", bookmark_type="funnels", params={"events": [{"event": "Signup"}]}, dashboard_id=12345, ) data = params.model_dump(by_alias=True, exclude_none=True) ``` ### name ``` name: str ``` Bookmark name (required). ### bookmark_type ``` bookmark_type: BookmarkTypeLiteral = Field(alias='type') ``` Report type (required, serialized as `"type"`). Pydantic-validated against the canonical set on construction β€” typos like `"insightz"` are rejected before any API call. ### params ``` params: dict[str, Any] ``` Query parameters (required). ### description ``` description: str | None = None ``` Bookmark description. ### icon ``` icon: str | None = None ``` Bookmark icon. ### dashboard_id ``` dashboard_id: int | None = None ``` Dashboard to associate with. ### is_visibility_restricted ``` is_visibility_restricted: bool | None = None ``` Visibility restriction flag. ### is_modification_restricted ``` is_modification_restricted: bool | None = None ``` Modification restriction flag. ## mixpanel_headless.UpdateBookmarkParams Bases: `BaseModel` Parameters for updating an existing bookmark/report. All fields are optional β€” only provided fields are sent to the API. | ATTRIBUTE | DESCRIPTION | | ---------------------------- | ------------------------------------------------ | | `name` | New bookmark name. **TYPE:** \`str | | `params` | New query parameters. **TYPE:** \`dict[str, Any] | | `description` | New bookmark description. **TYPE:** \`str | | `icon` | New bookmark icon. **TYPE:** \`str | | `dashboard_id` | New associated dashboard ID. **TYPE:** \`int | | `is_visibility_restricted` | New visibility restriction. **TYPE:** \`bool | | `is_modification_restricted` | New modification restriction. **TYPE:** \`bool | | `deleted` | Soft-delete flag. **TYPE:** \`bool | Example ``` params = UpdateBookmarkParams(name="Updated Funnel") data = params.model_dump(exclude_none=True) # {"name": "Updated Funnel"} ``` ### name ``` name: str | None = None ``` New bookmark name. ### params ``` params: dict[str, Any] | None = None ``` New query parameters. ### description ``` description: str | None = None ``` New bookmark description. ### icon ``` icon: str | None = None ``` New bookmark icon. ### dashboard_id ``` dashboard_id: int | None = None ``` New associated dashboard ID. ### is_visibility_restricted ``` is_visibility_restricted: bool | None = None ``` New visibility restriction. ### is_modification_restricted ``` is_modification_restricted: bool | None = None ``` New modification restriction. ### deleted ``` deleted: bool | None = None ``` Soft-delete flag. ## mixpanel_headless.BulkUpdateBookmarkEntry Bases: `BaseModel` Entry for bulk-updating bookmarks. | ATTRIBUTE | DESCRIPTION | | ---------------------------- | ------------------------------------------------- | | `id` | Bookmark ID to update (required). **TYPE:** `int` | | `name` | New bookmark name. **TYPE:** \`str | | `params` | New query parameters. **TYPE:** \`dict[str, Any] | | `description` | New bookmark description. **TYPE:** \`str | | `icon` | New bookmark icon. **TYPE:** \`str | | `is_visibility_restricted` | New visibility restriction. **TYPE:** \`bool | | `is_modification_restricted` | New modification restriction. **TYPE:** \`bool | Example ``` entry = BulkUpdateBookmarkEntry(id=123, name="Renamed") ``` ### id ``` id: int ``` Bookmark ID to update (required). ### name ``` name: str | None = None ``` New bookmark name. ### params ``` params: dict[str, Any] | None = None ``` New query parameters. ### description ``` description: str | None = None ``` New bookmark description. ### icon ``` icon: str | None = None ``` New bookmark icon. ### is_visibility_restricted ``` is_visibility_restricted: bool | None = None ``` New visibility restriction. ### is_modification_restricted ``` is_modification_restricted: bool | None = None ``` New modification restriction. ## mixpanel_headless.BookmarkHistoryResponse Bases: `BaseModel` Response from the bookmark history endpoint. | ATTRIBUTE | DESCRIPTION | | ------------ | ---------------------------------------------------------- | | `results` | List of history entries. **TYPE:** `list[Any]` | | `pagination` | Pagination metadata. **TYPE:** \`BookmarkHistoryPagination | Example ``` response = BookmarkHistoryResponse(results=[{"action": "created"}]) ``` ### results ``` results: list[Any] = Field(default_factory=list) ``` List of history entries. ### pagination ``` pagination: BookmarkHistoryPagination | None = None ``` Pagination metadata. ## mixpanel_headless.BookmarkHistoryPagination Bases: `BaseModel` Pagination metadata for bookmark history responses. | ATTRIBUTE | DESCRIPTION | | ----------------- | ----------------------------------------- | | `next_cursor` | Cursor for next page. **TYPE:** \`str | | `previous_cursor` | Cursor for previous page. **TYPE:** \`str | | `page_size` | Number of items per page. **TYPE:** `int` | Example ``` pagination = BookmarkHistoryPagination(page_size=20) ``` ### next_cursor ``` next_cursor: str | None = None ``` Cursor for next page. ### previous_cursor ``` previous_cursor: str | None = None ``` Cursor for previous page. ### page_size ``` page_size: int = 0 ``` Number of items per page. ## Cohort CRUD Types ## mixpanel_headless.Cohort Bases: `BaseModel` A Mixpanel cohort as returned by the App API. Represents the full cohort entity with definition, metadata, and cross-references. Extra fields from API evolution are preserved via `extra="allow"`. | ATTRIBUTE | DESCRIPTION | | ------------------------ | ----------------------------------------------------------------------- | | `id` | Unique cohort identifier. **TYPE:** `int` | | `name` | Cohort name. **TYPE:** `str` | | `description` | Cohort description. **TYPE:** \`str | | `count` | Number of users in the cohort. **TYPE:** \`int | | `is_visible` | Whether the cohort is visible. **TYPE:** \`bool | | `is_locked` | Whether the cohort is locked. **TYPE:** \`bool | | `data_group_id` | Data group identifier. **TYPE:** \`str | | `last_edited` | Last edited timestamp string. **TYPE:** \`str | | `created_by` | Creator information. **TYPE:** \`CohortCreator | | `referenced_by` | IDs of entities referencing this cohort. **TYPE:** \`list[int] | | `verified` | Whether the cohort is verified. **TYPE:** `bool` | | `last_queried` | Last queried timestamp string. **TYPE:** \`str | | `referenced_directly_by` | IDs of entities directly referencing this cohort. **TYPE:** `list[int]` | | `active_integrations` | Active integration IDs. **TYPE:** `list[int]` | Example ``` cohort = Cohort(id=1, name="Power Users") assert cohort.name == "Power Users" ``` ### id ``` id: int ``` Unique cohort identifier. ### name ``` name: str ``` Cohort name. ### description ``` description: str | None = None ``` Cohort description. ### count ``` count: int | None = None ``` Number of users in the cohort. ### is_visible ``` is_visible: bool | None = None ``` Whether the cohort is visible. ### is_locked ``` is_locked: bool | None = None ``` Whether the cohort is locked. ### data_group_id ``` data_group_id: str | None = None ``` Data group identifier. ### last_edited ``` last_edited: str | None = None ``` Last edited timestamp string. ### created_by ``` created_by: CohortCreator | None = None ``` Creator information. ### referenced_by ``` referenced_by: list[int] | None = None ``` IDs of entities referencing this cohort. ### verified ``` verified: bool = False ``` Whether the cohort is verified. ### last_queried ``` last_queried: str | None = None ``` Last queried timestamp string. ### referenced_directly_by ``` referenced_directly_by: list[int] = Field(default_factory=list) ``` IDs of entities directly referencing this cohort. ### active_integrations ``` active_integrations: list[int] = Field(default_factory=list) ``` Active integration IDs. ## mixpanel_headless.CohortCreator Bases: `BaseModel` Creator information for a cohort. | ATTRIBUTE | DESCRIPTION | | --------- | -------------------------------- | | `id` | Creator user ID. **TYPE:** \`int | | `name` | Creator name. **TYPE:** \`str | | `email` | Creator email. **TYPE:** \`str | Example ``` creator = CohortCreator(id=1, name="Alice", email="alice@example.com") ``` ### id ``` id: int | None = None ``` Creator user ID. ### name ``` name: str | None = None ``` Creator name. ### email ``` email: str | None = None ``` Creator email. ## mixpanel_headless.CreateCohortParams Bases: `_DefinitionFlatteningModel` Parameters for creating a new cohort. The `definition` dict is flattened into the top-level JSON payload at serialization time β€” its keys become top-level fields in the request body. | ATTRIBUTE | DESCRIPTION | | --------------- | ------------------------------------------------------ | | `name` | Cohort name (required). **TYPE:** `str` | | `description` | Cohort description. **TYPE:** \`str | | `data_group_id` | Data group identifier. **TYPE:** \`str | | `is_locked` | Whether the cohort should be locked. **TYPE:** \`bool | | `is_visible` | Whether the cohort should be visible. **TYPE:** \`bool | | `deleted` | Soft-delete flag. **TYPE:** \`bool | Example ``` params = CreateCohortParams(name="Power Users") data = params.model_dump(exclude_none=True) # {"name": "Power Users"} ``` ### name ``` name: str ``` Cohort name (required). ### description ``` description: str | None = None ``` Cohort description. ### data_group_id ``` data_group_id: str | None = None ``` Data group identifier. ### is_locked ``` is_locked: bool | None = None ``` Whether the cohort should be locked. ### is_visible ``` is_visible: bool | None = None ``` Whether the cohort should be visible. ### deleted ``` deleted: bool | None = None ``` Soft-delete flag. ## mixpanel_headless.UpdateCohortParams Bases: `_DefinitionFlatteningModel` Parameters for updating an existing cohort. All fields are optional β€” only provided fields are sent to the API. The `definition` dict is flattened into the payload. | ATTRIBUTE | DESCRIPTION | | --------------- | ------------------------------------------ | | `name` | New cohort name. **TYPE:** \`str | | `description` | New cohort description. **TYPE:** \`str | | `data_group_id` | New data group identifier. **TYPE:** \`str | | `is_locked` | New lock setting. **TYPE:** \`bool | | `is_visible` | New visibility setting. **TYPE:** \`bool | | `deleted` | Soft-delete flag. **TYPE:** \`bool | Example ``` params = UpdateCohortParams(name="Updated Cohort") data = params.model_dump(exclude_none=True) # {"name": "Updated Cohort"} ``` ### name ``` name: str | None = None ``` New cohort name. ### description ``` description: str | None = None ``` New cohort description. ### data_group_id ``` data_group_id: str | None = None ``` New data group identifier. ### is_locked ``` is_locked: bool | None = None ``` New lock setting. ### is_visible ``` is_visible: bool | None = None ``` New visibility setting. ### deleted ``` deleted: bool | None = None ``` Soft-delete flag. ## mixpanel_headless.BulkUpdateCohortEntry Bases: `_DefinitionFlatteningModel` Entry for bulk-updating cohorts. | ATTRIBUTE | DESCRIPTION | | ------------- | ----------------------------------------------- | | `id` | Cohort ID to update (required). **TYPE:** `int` | | `name` | New cohort name. **TYPE:** \`str | | `description` | New cohort description. **TYPE:** \`str | Example ``` entry = BulkUpdateCohortEntry(id=1, name="Renamed") ``` ### id ``` id: int ``` Cohort ID to update (required). ### name ``` name: str | None = None ``` New cohort name. ### description ``` description: str | None = None ``` New cohort description. ## Feature Flag Enums ## mixpanel_headless.FeatureFlagStatus Bases: `str`, `Enum` Lifecycle state of a feature flag. | ATTRIBUTE | DESCRIPTION | | ---------- | ----------------------------------------------------- | | `ENABLED` | Flag is active and serving variants. | | `DISABLED` | Flag is inactive (default state). | | `ARCHIVED` | Flag is soft-deleted, excluded from default listings. | Example ``` status = FeatureFlagStatus.ENABLED assert status.value == "enabled" ``` ## mixpanel_headless.ServingMethod Bases: `str`, `Enum` Controls how flag values are delivered to clients. | ATTRIBUTE | DESCRIPTION | | ----------------- | --------------------------------- | | `CLIENT` | Client-side evaluation (default). | | `SERVER` | Server-side evaluation only. | | `REMOTE_OR_LOCAL` | Remote preferred, local fallback. | | `REMOTE_ONLY` | Remote evaluation only. | Example ``` method = ServingMethod.CLIENT assert method.value == "client" ``` ## mixpanel_headless.FlagContractStatus Bases: `str`, `Enum` Account-level flag contract status. | ATTRIBUTE | DESCRIPTION | | -------------- | ------------------------- | | `ACTIVE` | Active contract. | | `GRACE_PERIOD` | Contract in grace period. | | `EXPIRED` | Contract expired. | Example ``` status = FlagContractStatus.ACTIVE assert status.value == "active" ``` ## Feature Flag Types ## mixpanel_headless.FeatureFlag Bases: `BaseModel` A Mixpanel feature flag as returned by the App API. Represents the full feature flag entity including configuration, metadata, and permissions. Extra fields from API evolution are preserved via `extra="allow"`. | ATTRIBUTE | DESCRIPTION | | ------------------------ | ----------------------------------------------------------------------- | | `id` | Unique identifier (UUID). **TYPE:** `str` | | `project_id` | Project this flag belongs to. **TYPE:** `int` | | `name` | Human-readable name. **TYPE:** `str` | | `key` | Machine-readable key (unique per project). **TYPE:** `str` | | `description` | Optional description. **TYPE:** \`str | | `status` | Current lifecycle status. **TYPE:** `FeatureFlagStatus` | | `tags` | Tags for organization. **TYPE:** `list[str]` | | `experiment_id` | Linked experiment ID if flag backs an experiment. **TYPE:** \`str | | `context` | Flag context identifier. **TYPE:** `str` | | `data_group_id` | Data group identifier. **TYPE:** \`str | | `serving_method` | How flag values are delivered. **TYPE:** `ServingMethod` | | `ruleset` | Variants, rollout rules, and test overrides. **TYPE:** `dict[str, Any]` | | `hash_salt` | Salt for deterministic variant assignment. **TYPE:** \`str | | `workspace_id` | Workspace this flag belongs to. **TYPE:** \`int | | `content_type` | Content type identifier. **TYPE:** \`str | | `created` | ISO 8601 creation timestamp. **TYPE:** `str` | | `modified` | ISO 8601 last-modified timestamp. **TYPE:** `str` | | `enabled_at` | Timestamp when flag was last enabled. **TYPE:** \`str | | `deleted` | Timestamp when flag was deleted. **TYPE:** \`str | | `creator_id` | Creator's user ID. **TYPE:** \`int | | `creator_name` | Creator's display name. **TYPE:** \`str | | `creator_email` | Creator's email. **TYPE:** \`str | | `last_modified_by_id` | Last modifier's user ID. **TYPE:** \`int | | `last_modified_by_name` | Last modifier's display name. **TYPE:** \`str | | `last_modified_by_email` | Last modifier's email. **TYPE:** \`str | | `is_favorited` | Whether current user has favorited. **TYPE:** \`bool | | `pinned_date` | Date flag was pinned. **TYPE:** \`str | | `can_edit` | Permission: can current user edit. **TYPE:** `bool` | Example ``` flag = FeatureFlag( id="abc-123", project_id=12345, name="Dark Mode", key="dark_mode", status=FeatureFlagStatus.DISABLED, context="default", serving_method=ServingMethod.CLIENT, ruleset={"variants": []}, created="2026-01-01T00:00:00Z", modified="2026-01-01T00:00:00Z", ) assert flag.key == "dark_mode" ``` ### id ``` id: str ``` Unique identifier (UUID). ### project_id ``` project_id: int ``` Project this flag belongs to. ### name ``` name: str ``` Human-readable name. ### key ``` key: str ``` Machine-readable key (unique per project). ### description ``` description: str | None = None ``` Optional description. ### status ``` status: FeatureFlagStatus = DISABLED ``` Current lifecycle status. ### tags ``` tags: list[str] = Field(default_factory=list) ``` Tags for organization. ### experiment_id ``` experiment_id: str | None = None ``` Linked experiment ID if flag backs an experiment. ### context ``` context: str = '' ``` Flag context identifier. ### data_group_id ``` data_group_id: str | None = None ``` Data group identifier. ### serving_method ``` serving_method: ServingMethod = CLIENT ``` How flag values are delivered. ### ruleset ``` ruleset: dict[str, Any] = Field(default_factory=dict) ``` Variants, rollout rules, and test overrides. ### hash_salt ``` hash_salt: str | None = None ``` Salt for deterministic variant assignment. ### workspace_id ``` workspace_id: int | None = None ``` Workspace this flag belongs to. ### content_type ``` content_type: str | None = None ``` Content type identifier. ### created ``` created: str = '' ``` ISO 8601 creation timestamp. ### modified ``` modified: str = '' ``` ISO 8601 last-modified timestamp. ### enabled_at ``` enabled_at: str | None = None ``` Timestamp when flag was last enabled. ### deleted ``` deleted: str | None = None ``` Timestamp when flag was deleted. ### creator_id ``` creator_id: int | None = None ``` Creator's user ID. ### creator_name ``` creator_name: str | None = None ``` Creator's display name. ### creator_email ``` creator_email: str | None = None ``` Creator's email. ### last_modified_by_id ``` last_modified_by_id: int | None = None ``` Last modifier's user ID. ### last_modified_by_name ``` last_modified_by_name: str | None = None ``` Last modifier's display name. ### last_modified_by_email ``` last_modified_by_email: str | None = None ``` Last modifier's email. ### is_favorited ``` is_favorited: bool | None = None ``` Whether current user has favorited. ### pinned_date ``` pinned_date: str | None = None ``` Date flag was pinned. ### can_edit ``` can_edit: bool = False ``` Permission: can current user edit. ## mixpanel_headless.CreateFeatureFlagParams Bases: `BaseModel` Parameters for creating a new feature flag. The Mixpanel API requires `name`, `key`, `context`, `serving_method`, `tags`, and `ruleset` (with `variants` and `rollout` sub-fields). Sensible defaults are provided for the non-obvious required fields so that minimal usage works:: ``` CreateFeatureFlagParams(name="Dark Mode", key="dark_mode") ``` | ATTRIBUTE | DESCRIPTION | | ---------------- | ------------------------------------------------------------------------------------------------------------------------ | | `name` | Flag name (required). **TYPE:** `str` | | `key` | Unique machine-readable key (required). **TYPE:** `str` | | `description` | Optional description. **TYPE:** \`str | | `status` | Initial status (defaults to disabled). **TYPE:** \`FeatureFlagStatus | | `tags` | Tags for organization (required by API, defaults to empty list). **TYPE:** `list[str]` | | `context` | Flag context identifier (required by API, defaults to "distinct_id"). **TYPE:** `str` | | `serving_method` | How flag values are delivered (required by API, defaults to ServingMethod.CLIENT). **TYPE:** `ServingMethod` | | `ruleset` | Ruleset with variants and rollout keys (required by API, defaults to a simple On/Off toggle). **TYPE:** `dict[str, Any]` | Example ``` params = CreateFeatureFlagParams(name="Dark Mode", key="dark_mode") data = params.model_dump(exclude_none=True) ``` ### name ``` name: str ``` Flag name (required). ### key ``` key: str ``` Unique machine-readable key (required). ### description ``` description: str | None = None ``` Optional description. ### status ``` status: FeatureFlagStatus | None = None ``` Initial status (defaults to disabled). ### tags ``` tags: list[str] = Field(default_factory=list) ``` Tags for organization (required by API, defaults to empty list). ### context ``` context: str = 'distinct_id' ``` Flag context identifier (required by API). ### serving_method ``` serving_method: ServingMethod = CLIENT ``` How flag values are delivered (required by API). ### ruleset ``` ruleset: dict[str, Any] = Field( default_factory=lambda: { "variants": [ { "key": "On", "value": True, "is_control": False, "split": 1.0, "is_sticky": False, }, { "key": "Off", "value": False, "is_control": True, "split": 0.0, "is_sticky": False, }, ], "rollout": [], } ) ``` Ruleset with variants and rollout (required by API). ## mixpanel_headless.UpdateFeatureFlagParams Bases: `BaseModel` Parameters for updating an existing feature flag (PUT semantics). All required fields must always be provided since this performs a full replacement, not a partial update. The API requires `tags`, `context`, and `serving_method` in addition to `name`, `key`, `status`, and `ruleset`. | ATTRIBUTE | DESCRIPTION | | ---------------- | ------------------------------------------------------------------------------------------------------------ | | `name` | Flag name (required). **TYPE:** `str` | | `key` | Unique key (required). **TYPE:** `str` | | `status` | Target status (required). **TYPE:** `FeatureFlagStatus` | | `ruleset` | Complete ruleset β€” replaces existing (required). **TYPE:** `dict[str, Any]` | | `description` | Optional description. **TYPE:** \`str | | `tags` | Tags for organization (required by API, defaults to empty list). **TYPE:** `list[str]` | | `context` | Flag context identifier (required by API, defaults to "distinct_id"). **TYPE:** `str` | | `serving_method` | How flag values are delivered (required by API, defaults to ServingMethod.CLIENT). **TYPE:** `ServingMethod` | Example ``` params = UpdateFeatureFlagParams( name="Dark Mode", key="dark_mode", status=FeatureFlagStatus.ENABLED, ruleset={"variants": [], "rollout": []}, ) ``` ### name ``` name: str ``` Flag name (required). ### key ``` key: str ``` Unique key (required). ### status ``` status: FeatureFlagStatus ``` Target status (required). ### ruleset ``` ruleset: dict[str, Any] ``` Complete ruleset β€” replaces existing (required). ### description ``` description: str | None = None ``` Optional description. ### tags ``` tags: list[str] = Field(default_factory=list) ``` Tags for organization (required by API, defaults to empty list). ### context ``` context: str = 'distinct_id' ``` Flag context identifier (required by API). ### serving_method ``` serving_method: ServingMethod = CLIENT ``` How flag values are delivered (required by API). ## mixpanel_headless.SetTestUsersParams Bases: `BaseModel` Parameters for setting test user variant overrides on a flag. | ATTRIBUTE | DESCRIPTION | | --------- | ------------------------------------------------------------------------ | | `users` | Mapping of variant keys to user distinct IDs. **TYPE:** `dict[str, str]` | Example ``` params = SetTestUsersParams(users={"on": "user-1", "off": "user-2"}) ``` ### users ``` users: dict[str, str] ``` Mapping of variant keys to user distinct IDs. ## mixpanel_headless.FlagHistoryParams Bases: `BaseModel` Parameters for querying feature flag change history. | ATTRIBUTE | DESCRIPTION | | ----------- | ---------------------------------- | | `page` | Pagination cursor. **TYPE:** \`str | | `page_size` | Results per page. **TYPE:** \`int | Example ``` params = FlagHistoryParams(page_size=50) ``` ### page ``` page: str | None = None ``` Pagination cursor. ### page_size ``` page_size: int | None = None ``` Results per page. ## mixpanel_headless.FlagHistoryResponse Bases: `BaseModel` Paginated change history for a feature flag. | ATTRIBUTE | DESCRIPTION | | --------- | -------------------------------------------------- | | `events` | Array of event arrays. **TYPE:** `list[list[Any]]` | | `count` | Total number of events. **TYPE:** `int` | Example ``` response = FlagHistoryResponse(events=[[1, "change"]], count=1) assert response.count == 1 ``` ### events ``` events: list[list[Any]] ``` Array of event arrays. ### count ``` count: int ``` Total number of events. ## mixpanel_headless.FlagLimitsResponse Bases: `BaseModel` Account-level feature flag usage and limits. | ATTRIBUTE | DESCRIPTION | | ----------------- | ----------------------------------------------- | | `limit` | Maximum allowed flags. **TYPE:** `int` | | `is_trial` | Whether account is on trial. **TYPE:** `bool` | | `current_usage` | Current number of flags. **TYPE:** `int` | | `contract_status` | Contract status. **TYPE:** `FlagContractStatus` | Example ``` limits = FlagLimitsResponse( limit=100, is_trial=False, current_usage=42, contract_status=FlagContractStatus.ACTIVE, ) assert limits.current_usage == 42 ``` ### limit ``` limit: int ``` Maximum allowed flags. ### is_trial ``` is_trial: bool ``` Whether account is on trial. ### current_usage ``` current_usage: int ``` Current number of flags. ### contract_status ``` contract_status: FlagContractStatus ``` Contract status. ## Experiment Enums ## mixpanel_headless.ExperimentStatus Bases: `str`, `Enum` Lifecycle state of an experiment. State transitions: `draft` β†’ `active` (launch) β†’ `concluded` (conclude) β†’ `success` | `fail` (decide). | ATTRIBUTE | DESCRIPTION | | ----------- | -------------------------------------- | | `DRAFT` | Experiment created but not started. | | `ACTIVE` | Experiment running, collecting data. | | `CONCLUDED` | Experiment stopped, awaiting decision. | | `SUCCESS` | Experiment decided as successful. | | `FAIL` | Experiment decided as failed. | Example ``` status = ExperimentStatus.DRAFT assert status.value == "draft" ``` ## Experiment Types ## mixpanel_headless.ExperimentCreator Bases: `BaseModel` Creator metadata for an experiment. | ATTRIBUTE | DESCRIPTION | | ------------ | ------------------------------------- | | `id` | Creator's user ID. **TYPE:** \`int | | `first_name` | Creator's first name. **TYPE:** \`str | | `last_name` | Creator's last name. **TYPE:** \`str | Example ``` creator = ExperimentCreator(id=1, first_name="Alice", last_name="Smith") ``` ### id ``` id: int | None = None ``` Creator's user ID. ### first_name ``` first_name: str | None = None ``` Creator's first name. ### last_name ``` last_name: str | None = None ``` Creator's last name. ## mixpanel_headless.Experiment Bases: `BaseModel` A Mixpanel A/B experiment as returned by the App API. Represents the full experiment entity including lifecycle state, variants, metrics, and metadata. Extra fields from API evolution are preserved via `extra="allow"`. | ATTRIBUTE | DESCRIPTION | | ------------------------ | ------------------------------------------------------ | | `id` | Unique identifier (UUID). **TYPE:** `str` | | `name` | Human-readable name. **TYPE:** `str` | | `description` | Optional description. **TYPE:** \`str | | `hypothesis` | Experiment hypothesis. **TYPE:** \`str | | `status` | Current lifecycle status. **TYPE:** \`ExperimentStatus | | `variants` | Variant configuration. **TYPE:** \`list[Any] | | `metrics` | Success metrics. **TYPE:** \`list[Any] | | `settings` | Experiment settings. **TYPE:** \`dict[str, Any] | | `exposures_cache` | Cached exposure data. **TYPE:** \`dict[str, Any] | | `results_cache` | Cached result data. **TYPE:** \`dict[str, Any] | | `start_date` | ISO 8601 start date. **TYPE:** \`str | | `end_date` | ISO 8601 end date. **TYPE:** \`str | | `created` | ISO 8601 creation timestamp. **TYPE:** \`str | | `updated` | ISO 8601 last-updated timestamp. **TYPE:** \`str | | `creator` | Creator metadata. **TYPE:** \`ExperimentCreator | | `feature_flag` | Linked feature flag data. **TYPE:** \`dict[str, Any] | | `is_favorited` | Whether current user has favorited. **TYPE:** \`bool | | `pinned_date` | Date experiment was pinned. **TYPE:** \`str | | `tags` | Tags for organization. **TYPE:** \`list[str] | | `can_edit` | Permission: can current user edit. **TYPE:** \`bool | | `last_modified_by_id` | Last modifier's user ID. **TYPE:** \`int | | `last_modified_by_name` | Last modifier's display name. **TYPE:** \`str | | `last_modified_by_email` | Last modifier's email. **TYPE:** \`str | Example ``` exp = Experiment(id="xyz-456", name="Checkout Flow Test") assert exp.name == "Checkout Flow Test" ``` ### id ``` id: str ``` Unique identifier (UUID). ### name ``` name: str ``` Human-readable name. ### description ``` description: str | None = None ``` Optional description. ### hypothesis ``` hypothesis: str | None = None ``` Experiment hypothesis. ### status ``` status: ExperimentStatus | None = None ``` Current lifecycle status. ### variants ``` variants: list[Any] | dict[str, Any] | None = None ``` Variant configuration (list from API, may also be dict). ### metrics ``` metrics: list[Any] | dict[str, Any] | None = None ``` Success metrics (list from API, may also be dict). ### settings ``` settings: dict[str, Any] | None = None ``` Experiment settings. ### exposures_cache ``` exposures_cache: dict[str, Any] | None = None ``` Cached exposure data. ### results_cache ``` results_cache: dict[str, Any] | None = None ``` Cached result data. ### start_date ``` start_date: str | None = None ``` ISO 8601 start date. ### end_date ``` end_date: str | None = None ``` ISO 8601 end date. ### created ``` created: str | None = None ``` ISO 8601 creation timestamp. ### updated ``` updated: str | None = None ``` ISO 8601 last-updated timestamp. ### creator ``` creator: ExperimentCreator | None = None ``` Creator metadata. ### feature_flag ``` feature_flag: dict[str, Any] | None = None ``` Linked feature flag data. ### is_favorited ``` is_favorited: bool | None = None ``` Whether current user has favorited. ### pinned_date ``` pinned_date: str | None = None ``` Date experiment was pinned. ### tags ``` tags: list[str] | None = None ``` Tags for organization. ### can_edit ``` can_edit: bool | None = None ``` Permission: can current user edit. ### last_modified_by_id ``` last_modified_by_id: int | None = None ``` Last modifier's user ID. ### last_modified_by_name ``` last_modified_by_name: str | None = None ``` Last modifier's display name. ### last_modified_by_email ``` last_modified_by_email: str | None = None ``` Last modifier's email. ## mixpanel_headless.CreateExperimentParams Bases: `BaseModel` Parameters for creating a new experiment. | ATTRIBUTE | DESCRIPTION | | ------------- | ----------------------------------------------- | | `name` | Experiment name (required). **TYPE:** `str` | | `description` | Optional description. **TYPE:** \`str | | `hypothesis` | Experiment hypothesis. **TYPE:** \`str | | `settings` | Experiment settings. **TYPE:** \`dict[str, Any] | | `access_type` | Access control type. **TYPE:** \`str | | `can_edit` | Edit permission. **TYPE:** \`bool | Example ``` params = CreateExperimentParams(name="Checkout Flow Test") data = params.model_dump(exclude_none=True) # {"name": "Checkout Flow Test"} ``` ### name ``` name: str ``` Experiment name (required). ### description ``` description: str | None = None ``` Optional description. ### hypothesis ``` hypothesis: str | None = None ``` Experiment hypothesis. ### settings ``` settings: dict[str, Any] | None = None ``` Experiment settings. ### access_type ``` access_type: str | None = None ``` Access control type. ### can_edit ``` can_edit: bool | None = None ``` Edit permission. ## mixpanel_headless.UpdateExperimentParams Bases: `BaseModel` Parameters for updating an existing experiment (PATCH semantics). All fields optional β€” only provided fields are updated. | ATTRIBUTE | DESCRIPTION | | -------------------- | --------------------------------------------------- | | `name` | Updated name. **TYPE:** \`str | | `description` | Updated description. **TYPE:** \`str | | `hypothesis` | Updated hypothesis. **TYPE:** \`str | | `variants` | Updated variant config. **TYPE:** \`list[Any] | | `metrics` | Updated metrics. **TYPE:** \`list[Any] | | `settings` | Updated settings. **TYPE:** \`dict[str, Any] | | `start_date` | Updated start date. **TYPE:** \`str | | `end_date` | Updated end date. **TYPE:** \`str | | `tags` | Updated tags. **TYPE:** \`list[str] | | `exposures_cache` | Updated exposures cache. **TYPE:** \`dict[str, Any] | | `results_cache` | Updated results cache. **TYPE:** \`dict[str, Any] | | `status` | Updated status. **TYPE:** \`ExperimentStatus | | `global_access_type` | Updated access type. **TYPE:** \`str | Example ``` params = UpdateExperimentParams(description="Updated") data = params.model_dump(exclude_none=True) # {"description": "Updated"} ``` ### name ``` name: str | None = None ``` Updated name. ### description ``` description: str | None = None ``` Updated description. ### hypothesis ``` hypothesis: str | None = None ``` Updated hypothesis. ### variants ``` variants: list[Any] | dict[str, Any] | None = None ``` Updated variant config (list or dict). ### metrics ``` metrics: list[Any] | dict[str, Any] | None = None ``` Updated metrics (list or dict). ### settings ``` settings: dict[str, Any] | None = None ``` Updated settings. ### start_date ``` start_date: str | None = None ``` Updated start date. ### end_date ``` end_date: str | None = None ``` Updated end date. ### tags ``` tags: list[str] | None = None ``` Updated tags. ### exposures_cache ``` exposures_cache: dict[str, Any] | None = None ``` Updated exposures cache. ### results_cache ``` results_cache: dict[str, Any] | None = None ``` Updated results cache. ### status ``` status: ExperimentStatus | None = None ``` Updated status. ### global_access_type ``` global_access_type: str | None = None ``` Updated access type. ## mixpanel_headless.ExperimentConcludeParams Bases: `BaseModel` Parameters for concluding an experiment. | ATTRIBUTE | DESCRIPTION | | ---------- | --------------------------------------------- | | `end_date` | Override end date (ISO 8601). **TYPE:** \`str | Example ``` params = ExperimentConcludeParams(end_date="2026-04-01") ``` ### end_date ``` end_date: str | None = None ``` Override end date (ISO 8601). ## mixpanel_headless.ExperimentDecideParams Bases: `BaseModel` Parameters for recording an experiment decision. | ATTRIBUTE | DESCRIPTION | | --------- | ------------------------------------------------------------- | | `success` | Whether the experiment succeeded (required). **TYPE:** `bool` | | `variant` | Winning variant key. **TYPE:** \`str | | `message` | Decision summary message. **TYPE:** \`str | Example ``` params = ExperimentDecideParams(success=True, variant="simplified") ``` ### success ``` success: bool ``` Whether the experiment succeeded (required). ### variant ``` variant: str | None = None ``` Winning variant key. ### message ``` message: str | None = None ``` Decision summary message. ## mixpanel_headless.DuplicateExperimentParams Bases: `BaseModel` Parameters for duplicating an experiment. | ATTRIBUTE | DESCRIPTION | | --------- | -------------------------------------------------------------- | | `name` | Name for the duplicated experiment (required). **TYPE:** `str` | Example ``` params = DuplicateExperimentParams(name="Checkout Flow Test v2") ``` ### name ``` name: str ``` Name for the duplicated experiment (required). ## Annotation Types ## mixpanel_headless.Annotation Bases: `BaseModel` Response model for a timeline annotation. | ATTRIBUTE | DESCRIPTION | | ------------- | ------------------------------------------------ | | `id` | Annotation ID. **TYPE:** `int` | | `project_id` | Project ID. **TYPE:** `int` | | `date` | Annotation date (ISO format). **TYPE:** `str` | | `description` | Annotation text. **TYPE:** `str` | | `user` | Creator user info. **TYPE:** \`AnnotationUser | | `tags` | Associated tags. **TYPE:** `list[AnnotationTag]` | Example ``` annotation = Annotation.model_validate(api_response) ``` ### id ``` id: int ``` Annotation ID. ### project_id ``` project_id: int ``` Project ID. ### date ``` date: str ``` Annotation date (`%Y-%m-%d %H:%M:%S` format). ### description ``` description: str ``` Annotation text. ### user ``` user: AnnotationUser | None = None ``` Creator user info. ### tags ``` tags: list[AnnotationTag] = Field(default_factory=list) ``` Associated tags. ## mixpanel_headless.AnnotationUser Bases: `BaseModel` Nested user info for annotation creator. | ATTRIBUTE | DESCRIPTION | | ------------ | --------------------------- | | `id` | User ID. **TYPE:** `int` | | `first_name` | First name. **TYPE:** `str` | | `last_name` | Last name. **TYPE:** `str` | Example ``` user = AnnotationUser(id=1, first_name="Alice", last_name="Smith") ``` ### id ``` id: int ``` User ID. ### first_name ``` first_name: str ``` First name. ### last_name ``` last_name: str ``` Last name. ## mixpanel_headless.AnnotationTag Bases: `BaseModel` Annotation tag for categorization. | ATTRIBUTE | DESCRIPTION | | ----------------- | --------------------------------------------- | | `id` | Tag ID. **TYPE:** `int` | | `name` | Tag name. **TYPE:** `str` | | `project_id` | Project ID. **TYPE:** \`int | | `has_annotations` | Whether tag has annotations. **TYPE:** \`bool | Example ``` tag = AnnotationTag(id=1, name="releases") ``` ### id ``` id: int ``` Tag ID. ### name ``` name: str ``` Tag name. ### project_id ``` project_id: int | None = None ``` Project ID. ### has_annotations ``` has_annotations: bool | None = None ``` Whether tag has annotations. ## mixpanel_headless.CreateAnnotationParams Bases: `BaseModel` Parameters for creating a new annotation. | ATTRIBUTE | DESCRIPTION | | ------------- | ------------------------------------------------------------------- | | `date` | Date string in %Y-%m-%d %H:%M:%S format (required). **TYPE:** `str` | | `description` | Annotation text (max 512 characters, required). **TYPE:** `str` | | `tags` | Tag IDs to associate. **TYPE:** \`list[int] | | `user_id` | Creator user ID. **TYPE:** \`int | Example ``` params = CreateAnnotationParams( date="2026-03-31 00:00:00", description="v2.5 release" ) ``` ### date ``` date: str ``` Date string in `%Y-%m-%d %H:%M:%S` format. ### description ``` description: str = Field(max_length=512) ``` Annotation text (max 512 characters). ### tags ``` tags: list[int] | None = None ``` Tag IDs to associate. ### user_id ``` user_id: int | None = None ``` Creator user ID. ## mixpanel_headless.UpdateAnnotationParams Bases: `BaseModel` Parameters for updating an annotation (PATCH semantics). Only `description` and `tags` can be changed after creation; the annotation date is immutable. | ATTRIBUTE | DESCRIPTION | | ------------- | ----------------------------------------------------- | | `description` | New description (max 512 characters). **TYPE:** \`str | | `tags` | New tag IDs. **TYPE:** \`list[int] | Example ``` params = UpdateAnnotationParams(description="Updated text") ``` ### description ``` description: str | None = Field(default=None, max_length=512) ``` New description (max 512 characters). ### tags ``` tags: list[int] | None = None ``` New tag IDs. ## mixpanel_headless.CreateAnnotationTagParams Bases: `BaseModel` Parameters for creating an annotation tag. | ATTRIBUTE | DESCRIPTION | | --------- | ------------------------------------ | | `name` | Tag name (required). **TYPE:** `str` | Example ``` params = CreateAnnotationTagParams(name="releases") ``` ### name ``` name: str ``` Tag name. ## Webhook Enums ## mixpanel_headless.WebhookAuthType Bases: `str`, `Enum` Authentication type for webhooks. Values BASIC: HTTP Basic authentication. ## Webhook Types ## mixpanel_headless.ProjectWebhook Bases: `BaseModel` Response model for a project webhook. | ATTRIBUTE | DESCRIPTION | | -------------- | ------------------------------------------------ | | `id` | Webhook ID (UUID string). **TYPE:** `str` | | `name` | Webhook name. **TYPE:** `str` | | `url` | Webhook URL. **TYPE:** `str` | | `is_enabled` | Whether enabled. **TYPE:** `bool` | | `auth_type` | Authentication type. **TYPE:** \`WebhookAuthType | | `created` | Creation timestamp. **TYPE:** \`str | | `modified` | Last modified timestamp. **TYPE:** \`str | | `creator_id` | Creator user ID. **TYPE:** \`int | | `creator_name` | Creator name. **TYPE:** \`str | Example ``` webhook = ProjectWebhook.model_validate(api_response) ``` ### id ``` id: str ``` Webhook ID (UUID string). ### name ``` name: str ``` Webhook name. ### url ``` url: str ``` Webhook URL. ### is_enabled ``` is_enabled: bool ``` Whether enabled. ### auth_type ``` auth_type: WebhookAuthType | None = None ``` Authentication type. ### created ``` created: str | None = None ``` Creation timestamp. ### modified ``` modified: str | None = None ``` Last modified timestamp. ### creator_id ``` creator_id: int | None = None ``` Creator user ID. ### creator_name ``` creator_name: str | None = None ``` Creator name. ## mixpanel_headless.CreateWebhookParams Bases: `BaseModel` Parameters for creating a webhook. | ATTRIBUTE | DESCRIPTION | | ----------- | -------------------------------------------------------- | | `name` | Webhook name (required). **TYPE:** `str` | | `url` | Webhook URL (required). **TYPE:** `str` | | `auth_type` | Auth type ("basic" or None). **TYPE:** \`WebhookAuthType | | `username` | Basic auth username. **TYPE:** \`str | | `password` | Basic auth password. **TYPE:** \`str | Example ``` params = CreateWebhookParams( name="Pipeline webhook", url="https://example.com/webhook", ) ``` ### name ``` name: str ``` Webhook name. ### url ``` url: str ``` Webhook URL. ### auth_type ``` auth_type: WebhookAuthType | None = None ``` Auth type (e.g. WebhookAuthType.BASIC). ### username ``` username: str | None = None ``` Basic auth username. ### password ``` password: str | None = None ``` Basic auth password. ## mixpanel_headless.UpdateWebhookParams Bases: `BaseModel` Parameters for updating a webhook (PATCH semantics). | ATTRIBUTE | DESCRIPTION | | ------------ | ------------------------------------------ | | `name` | New name. **TYPE:** \`str | | `url` | New URL. **TYPE:** \`str | | `auth_type` | New auth type. **TYPE:** \`WebhookAuthType | | `username` | New username. **TYPE:** \`str | | `password` | New password. **TYPE:** \`str | | `is_enabled` | New enabled state. **TYPE:** \`bool | Example ``` params = UpdateWebhookParams(name="Updated name") ``` ### name ``` name: str | None = None ``` New name. ### url ``` url: str | None = None ``` New URL. ### auth_type ``` auth_type: WebhookAuthType | None = None ``` New auth type. ### username ``` username: str | None = None ``` New username. ### password ``` password: str | None = None ``` New password. ### is_enabled ``` is_enabled: bool | None = None ``` New enabled state. ## mixpanel_headless.WebhookTestParams Bases: `BaseModel` Parameters for testing webhook connectivity. | ATTRIBUTE | DESCRIPTION | | ----------- | --------------------------------------- | | `url` | URL to test (required). **TYPE:** `str` | | `name` | Webhook name. **TYPE:** \`str | | `auth_type` | Auth type. **TYPE:** \`WebhookAuthType | | `username` | Username for auth. **TYPE:** \`str | | `password` | Password for auth. **TYPE:** \`str | Example ``` params = WebhookTestParams(url="https://example.com/webhook") ``` ### url ``` url: str ``` URL to test. ### name ``` name: str | None = None ``` Webhook name. ### auth_type ``` auth_type: WebhookAuthType | None = None ``` Auth type. ### username ``` username: str | None = None ``` Username for auth. ### password ``` password: str | None = None ``` Password for auth. ## mixpanel_headless.WebhookTestResult Bases: `BaseModel` Response model for webhook connectivity test. | ATTRIBUTE | DESCRIPTION | | ------------- | ---------------------------------------- | | `success` | Whether test succeeded. **TYPE:** `bool` | | `status_code` | HTTP status code. **TYPE:** `int` | | `message` | Descriptive message. **TYPE:** `str` | Example ``` result = WebhookTestResult.model_validate(api_response) if result.success: print("Webhook is reachable") ``` ### success ``` success: bool ``` Whether test succeeded. ### status_code ``` status_code: int ``` HTTP status code. ### message ``` message: str ``` Descriptive message. ## mixpanel_headless.WebhookMutationResult Bases: `BaseModel` Response model for webhook create/update (returns id + name only). | ATTRIBUTE | DESCRIPTION | | --------- | ----------------------------- | | `id` | Webhook ID. **TYPE:** `str` | | `name` | Webhook name. **TYPE:** `str` | Example ``` result = WebhookMutationResult.model_validate(api_response) ``` ### id ``` id: str ``` Webhook ID. ### name ``` name: str ``` Webhook name. ## Alert Enums ## mixpanel_headless.AlertFrequencyPreset Bases: `int`, `Enum` Preset frequency values for alert check intervals. Values HOURLY: Check every hour (3600 seconds). DAILY: Check every day (86400 seconds). WEEKLY: Check every week (604800 seconds). ## Alert Types ## mixpanel_headless.CustomAlert Bases: `BaseModel` Response model for a custom alert. | ATTRIBUTE | DESCRIPTION | | ---------------------- | ----------------------------------------------------------- | | `id` | Alert ID. **TYPE:** `int` | | `name` | Alert name. **TYPE:** `str` | | `bookmark` | Linked saved report. **TYPE:** \`AlertBookmark | | `condition` | Trigger condition (opaque JSON). **TYPE:** `dict[str, Any]` | | `frequency` | Check frequency in seconds. **TYPE:** `int` | | `paused` | Whether alert is paused. **TYPE:** `bool` | | `subscriptions` | Notification targets. **TYPE:** `list[dict[str, Any]]` | | `notification_windows` | Notification window config. **TYPE:** \`dict[str, Any] | | `creator` | Creator user info. **TYPE:** \`AlertCreator | | `workspace` | Workspace metadata. **TYPE:** \`AlertWorkspace | | `project` | Project metadata. **TYPE:** \`AlertProject | | `created` | Creation timestamp. **TYPE:** `str` | | `modified` | Last modified timestamp. **TYPE:** `str` | | `last_checked` | Last check timestamp. **TYPE:** \`str | | `last_fired` | Last trigger timestamp. **TYPE:** \`str | | `valid` | Whether alert is valid. **TYPE:** `bool` | | `results` | Latest evaluation results. **TYPE:** \`dict[str, Any] | Example ``` alert = CustomAlert.model_validate(api_response) ``` ### id ``` id: int ``` Alert ID. ### name ``` name: str ``` Alert name. ### bookmark ``` bookmark: AlertBookmark | None = None ``` Linked saved report. ### condition ``` condition: dict[str, Any] = Field(default_factory=dict) ``` Trigger condition (opaque JSON). ### frequency ``` frequency: int = 0 ``` Check frequency in seconds. ### paused ``` paused: bool = False ``` Whether alert is paused. ### subscriptions ``` subscriptions: list[dict[str, Any]] = Field(default_factory=list) ``` Notification targets. ### notification_windows ``` notification_windows: dict[str, Any] | None = None ``` Notification window config. ### creator ``` creator: AlertCreator | None = None ``` Creator user info. ### workspace ``` workspace: AlertWorkspace | None = None ``` Workspace metadata. ### project ``` project: AlertProject | None = None ``` Project metadata. ### created ``` created: str = '' ``` Creation timestamp. ### modified ``` modified: str = '' ``` Last modified timestamp. ### last_checked ``` last_checked: str | None = None ``` Last check timestamp. ### last_fired ``` last_fired: str | None = None ``` Last trigger timestamp. ### valid ``` valid: bool = True ``` Whether alert is valid. ### results ``` results: dict[str, Any] | None = None ``` Latest evaluation results. ## mixpanel_headless.AlertBookmark Bases: `BaseModel` Nested bookmark info for an alert. | ATTRIBUTE | DESCRIPTION | | --------- | ------------------------------ | | `id` | Bookmark ID. **TYPE:** `int` | | `name` | Bookmark name. **TYPE:** \`str | | `type` | Bookmark type. **TYPE:** \`str | Example ``` bookmark = AlertBookmark(id=1, name="Daily Signups") ``` ### id ``` id: int ``` Bookmark ID. ### name ``` name: str | None = None ``` Bookmark name. ### type ``` type: str | None = None ``` Bookmark type. ## mixpanel_headless.AlertCreator Bases: `BaseModel` Nested creator info for an alert. | ATTRIBUTE | DESCRIPTION | | ------------ | --------------------------- | | `id` | User ID. **TYPE:** `int` | | `first_name` | First name. **TYPE:** \`str | | `last_name` | Last name. **TYPE:** \`str | | `email` | Email. **TYPE:** \`str | Example ``` creator = AlertCreator(id=1, email="alice@example.com") ``` ### id ``` id: int ``` User ID. ### first_name ``` first_name: str | None = None ``` First name. ### last_name ``` last_name: str | None = None ``` Last name. ### email ``` email: str | None = None ``` Email. ## mixpanel_headless.AlertWorkspace Bases: `BaseModel` Nested workspace info for an alert. | ATTRIBUTE | DESCRIPTION | | --------- | ------------------------------- | | `id` | Workspace ID. **TYPE:** `int` | | `name` | Workspace name. **TYPE:** \`str | Example ``` ws = AlertWorkspace(id=100, name="Production") ``` ### id ``` id: int ``` Workspace ID. ### name ``` name: str | None = None ``` Workspace name. ## mixpanel_headless.AlertProject Bases: `BaseModel` Nested project info for an alert. | ATTRIBUTE | DESCRIPTION | | --------- | ----------------------------- | | `id` | Project ID. **TYPE:** `int` | | `name` | Project name. **TYPE:** \`str | Example ``` proj = AlertProject(id=12345, name="My App") ``` ### id ``` id: int ``` Project ID. ### name ``` name: str | None = None ``` Project name. ## mixpanel_headless.CreateAlertParams Bases: `BaseModel` Parameters for creating a new alert. | ATTRIBUTE | DESCRIPTION | | ---------------------- | ----------------------------------------------------------------- | | `bookmark_id` | ID of linked bookmark (required). **TYPE:** `int` | | `name` | Alert name (required). **TYPE:** `str` | | `condition` | Trigger condition JSON (required). **TYPE:** `dict[str, Any]` | | `frequency` | Check frequency in seconds (required). **TYPE:** `int` | | `paused` | Start paused or active (required). **TYPE:** `bool` | | `subscriptions` | Notification targets (required). **TYPE:** `list[dict[str, Any]]` | | `notification_windows` | Notification window config. **TYPE:** \`dict[str, Any] | Example ``` params = CreateAlertParams( bookmark_id=12345, name="Daily signups drop", condition={ "keys": [{"header": "Signup", "value": "Signup"}], "type": "absolute", "op": "<", "value": 100, }, frequency=AlertFrequencyPreset.DAILY, paused=False, subscriptions=[{"type": "email", "value": "team@example.com"}], ) ``` ### bookmark_id ``` bookmark_id: int ``` ID of linked bookmark. ### name ``` name: str = Field(max_length=50) ``` Alert name (max 50 characters). ### condition ``` condition: dict[str, Any] ``` Trigger condition JSON. ### frequency ``` frequency: int ``` Check frequency in seconds. See `AlertFrequencyPreset` for common values. ### paused ``` paused: bool ``` Start paused or active. ### subscriptions ``` subscriptions: list[dict[str, Any]] ``` Notification targets. ### notification_windows ``` notification_windows: dict[str, Any] | None = None ``` Notification window config. ## mixpanel_headless.UpdateAlertParams Bases: `BaseModel` Parameters for updating an alert (PATCH semantics). | ATTRIBUTE | DESCRIPTION | | ---------------------- | ----------------------------------------------------- | | `name` | New name. **TYPE:** \`str | | `bookmark_id` | New bookmark ID. **TYPE:** \`int | | `condition` | New condition. **TYPE:** \`dict[str, Any] | | `frequency` | New frequency. **TYPE:** \`int | | `paused` | New pause state. **TYPE:** \`bool | | `subscriptions` | New subscriptions. **TYPE:** \`list\[dict[str, Any]\] | | `notification_windows` | New notification windows. **TYPE:** \`dict[str, Any] | Example ``` params = UpdateAlertParams(name="Updated alert", paused=True) ``` ### name ``` name: str | None = None ``` New name. ### bookmark_id ``` bookmark_id: int | None = None ``` New bookmark ID. ### condition ``` condition: dict[str, Any] | None = None ``` New condition. ### frequency ``` frequency: int | None = None ``` New frequency. ### paused ``` paused: bool | None = None ``` New pause state. ### subscriptions ``` subscriptions: list[dict[str, Any]] | None = None ``` New subscriptions. ### notification_windows ``` notification_windows: dict[str, Any] | None = None ``` New notification windows. ## mixpanel_headless.AlertCount Bases: `BaseModel` Response model for alert count and limits. | ATTRIBUTE | DESCRIPTION | | ---------------------- | ------------------------------------- | | `anomaly_alerts_count` | Current alert count. **TYPE:** `int` | | `alert_limit` | Account limit. **TYPE:** `int` | | `is_below_limit` | Whether below limit. **TYPE:** `bool` | Example ``` count = AlertCount.model_validate(api_response) if count.is_below_limit: print(f"{count.anomaly_alerts_count}/{count.alert_limit}") ``` ### anomaly_alerts_count ``` anomaly_alerts_count: int ``` Current alert count. ### alert_limit ``` alert_limit: int ``` Account limit. ### is_below_limit ``` is_below_limit: bool ``` Whether below limit. ## mixpanel_headless.AlertHistoryPagination Bases: `BaseModel` Pagination metadata for alert history. | ATTRIBUTE | DESCRIPTION | | ----------------- | ------------------------------------- | | `next_cursor` | Next page cursor. **TYPE:** \`str | | `previous_cursor` | Previous page cursor. **TYPE:** \`str | | `page_size` | Page size. **TYPE:** `int` | Example ``` pagination = AlertHistoryPagination(page_size=20) ``` ### next_cursor ``` next_cursor: str | None = None ``` Next page cursor. ### previous_cursor ``` previous_cursor: str | None = None ``` Previous page cursor. ### page_size ``` page_size: int = 20 ``` Page size. ## mixpanel_headless.AlertHistoryResponse Bases: `BaseModel` Response model for alert history (paginated). | ATTRIBUTE | DESCRIPTION | | ------------ | ------------------------------------------------------- | | `results` | History entries. **TYPE:** `list[dict[str, Any]]` | | `pagination` | Pagination metadata. **TYPE:** \`AlertHistoryPagination | Example ``` history = AlertHistoryResponse.model_validate(api_response) for entry in history.results: print(entry) ``` ### results ``` results: list[dict[str, Any]] = Field(default_factory=list) ``` History entries. ### pagination ``` pagination: AlertHistoryPagination | None = None ``` Pagination metadata. ## mixpanel_headless.AlertScreenshotResponse Bases: `BaseModel` Response model for alert screenshot URL. | ATTRIBUTE | DESCRIPTION | | ------------ | ---------------------------------------------- | | `signed_url` | Signed GCS URL for screenshot. **TYPE:** `str` | Example ``` resp = AlertScreenshotResponse.model_validate(api_response) print(resp.signed_url) ``` ### signed_url ``` signed_url: str ``` Signed GCS URL for screenshot. ## mixpanel_headless.AlertValidation Bases: `BaseModel` Per-alert validation result. | ATTRIBUTE | DESCRIPTION | | ------------ | ---------------------------------- | | `alert_id` | Alert ID. **TYPE:** `int` | | `alert_name` | Alert name. **TYPE:** `str` | | `valid` | Whether valid. **TYPE:** `bool` | | `reason` | Reason if invalid. **TYPE:** \`str | Example ``` v = AlertValidation(alert_id=1, alert_name="Test", valid=True) ``` ### alert_id ``` alert_id: int ``` Alert ID. ### alert_name ``` alert_name: str ``` Alert name. ### valid ``` valid: bool ``` Whether valid. ### reason ``` reason: str | None = None ``` Reason if invalid. ## mixpanel_headless.ValidateAlertsForBookmarkParams Bases: `BaseModel` Parameters for validating alerts against a bookmark. | ATTRIBUTE | DESCRIPTION | | ----------------- | ---------------------------------------------------------------------------------------- | | `alert_ids` | Alert IDs to validate (required). **TYPE:** `list[int]` | | `bookmark_type` | Bookmark type to validate against (required). **TYPE:** `Literal['insights', 'funnels']` | | `bookmark_params` | Bookmark params JSON (required). **TYPE:** `dict[str, Any]` | Example ``` params = ValidateAlertsForBookmarkParams( alert_ids=[1, 2], bookmark_type="insights", bookmark_params={"event": "Signup"}, ) ``` ### alert_ids ``` alert_ids: list[int] = Field(min_length=1) ``` Alert IDs to validate (must not be empty). ### bookmark_type ``` bookmark_type: Literal['insights', 'funnels'] ``` Bookmark type to validate against. ### bookmark_params ``` bookmark_params: dict[str, Any] ``` Bookmark params JSON. ## mixpanel_headless.ValidateAlertsForBookmarkResponse Bases: `BaseModel` Response model for alert-bookmark validation. | ATTRIBUTE | DESCRIPTION | | ------------------- | --------------------------------------------------------------- | | `alert_validations` | Per-alert validation results. **TYPE:** `list[AlertValidation]` | | `invalid_count` | Count of invalid alerts. **TYPE:** `int` | Example ``` resp = ValidateAlertsForBookmarkResponse.model_validate(api_response) if resp.invalid_count > 0: for v in resp.alert_validations: if not v.valid: print(f"{v.alert_name}: {v.reason}") ``` ### alert_validations ``` alert_validations: list[AlertValidation] = Field(default_factory=list) ``` Per-alert validation results. ### invalid_count ``` invalid_count: int = 0 ``` Count of invalid alerts. ## Data Governance Enums ## mixpanel_headless.PropertyResourceType Bases: `str`, `Enum` Resource type for property definitions. Values EVENT: Event property. USER: User profile property. GROUPPROFILE: Group profile property (wire format: `groupprofile`). ## mixpanel_headless.CustomPropertyResourceType Bases: `str`, `Enum` Resource type for custom properties. Values EVENTS: Event-level custom property. PEOPLE: User profile custom property. GROUP_PROFILES: Group profile custom property. ## Event Definition Types ## mixpanel_headless.EventDefinition Bases: `BaseModel` A Mixpanel event definition from the Lexicon. | ATTRIBUTE | DESCRIPTION | | ----------------- | ------------------------------------------------------ | | `id` | Server-assigned event ID. **TYPE:** `int` | | `name` | Event name (unique identifier). **TYPE:** `str` | | `display_name` | Human-readable name. **TYPE:** \`str | | `description` | Event description. **TYPE:** \`str | | `hidden` | Whether hidden from UI. **TYPE:** \`bool | | `dropped` | Whether data is dropped at ingestion. **TYPE:** \`bool | | `merged` | Whether merged into another event. **TYPE:** \`bool | | `verified` | Whether verified by governance team. **TYPE:** \`bool | | `tags` | Assigned tag names. **TYPE:** \`list[str] | | `custom_event_id` | Links to custom event. **TYPE:** \`int | | `last_modified` | ISO 8601 timestamp. **TYPE:** \`str | | `status` | Event status. **TYPE:** \`str | | `platforms` | Tracking platforms. **TYPE:** \`list[str] | | `created_utc` | ISO 8601 creation timestamp. **TYPE:** \`str | | `modified_utc` | ISO 8601 modification timestamp. **TYPE:** \`str | Example ``` ev = EventDefinition(id=1, name="Purchase") ``` ### id ``` id: int ``` Server-assigned event ID. ### name ``` name: str ``` Event name (unique identifier). ### display_name ``` display_name: str | None = None ``` Human-readable name. ### description ``` description: str | None = None ``` Event description. ### hidden ``` hidden: bool | None = None ``` Whether hidden from UI. ### dropped ``` dropped: bool | None = None ``` Whether data is dropped at ingestion. ### merged ``` merged: bool | None = None ``` Whether merged into another event. ### verified ``` verified: bool | None = None ``` Whether verified by governance team. ### tags ``` tags: list[str] | None = None ``` Assigned tag names. ### custom_event_id ``` custom_event_id: int | None = None ``` Links to custom event. ### last_modified ``` last_modified: str | None = None ``` ISO 8601 timestamp. ### status ``` status: str | None = None ``` Event status. ### platforms ``` platforms: list[str] | None = None ``` Tracking platforms. ### created_utc ``` created_utc: str | None = None ``` ISO 8601 creation timestamp. ### modified_utc ``` modified_utc: str | None = None ``` ISO 8601 modification timestamp. ## mixpanel_headless.UpdateEventDefinitionParams Bases: `BaseModel` Parameters for updating an event definition (PATCH semantics). All fields are optional; only set fields are sent. | ATTRIBUTE | DESCRIPTION | | ------------- | ------------------------------------------ | | `hidden` | Whether hidden from UI. **TYPE:** \`bool | | `dropped` | Whether data is dropped. **TYPE:** \`bool | | `merged` | Whether merged. **TYPE:** \`bool | | `verified` | Whether verified. **TYPE:** \`bool | | `tags` | Tag names to assign. **TYPE:** \`list[str] | | `description` | Event description. **TYPE:** \`str | Example ``` params = UpdateEventDefinitionParams( description="User completed a purchase", verified=True ) ``` ### hidden ``` hidden: bool | None = None ``` Whether hidden from UI. ### dropped ``` dropped: bool | None = None ``` Whether data is dropped. ### merged ``` merged: bool | None = None ``` Whether merged. ### verified ``` verified: bool | None = None ``` Whether verified. ### tags ``` tags: list[str] | None = None ``` Tag names to assign. ### description ``` description: str | None = None ``` Event description. ## mixpanel_headless.BulkEventUpdate Bases: `BaseModel` A single event update entry for bulk operations. | ATTRIBUTE | DESCRIPTION | | --------------- | ------------------------------------------ | | `name` | Event name (identifier). **TYPE:** \`str | | `id` | Alternative identifier. **TYPE:** \`int | | `hidden` | Whether hidden from UI. **TYPE:** \`bool | | `dropped` | Whether data is dropped. **TYPE:** \`bool | | `merged` | Whether merged. **TYPE:** \`bool | | `verified` | Whether verified. **TYPE:** \`bool | | `tags` | Tag names. **TYPE:** \`list[str] | | `contacts` | Contact emails. **TYPE:** \`list[str] | | `team_contacts` | Team contact emails. **TYPE:** \`list[str] | Example ``` entry = BulkEventUpdate(name="OldEvent", hidden=True) ``` ### name ``` name: str | None = None ``` Event name (identifier). ### id ``` id: int | None = None ``` Alternative identifier. ### hidden ``` hidden: bool | None = None ``` Whether hidden from UI. ### dropped ``` dropped: bool | None = None ``` Whether data is dropped. ### merged ``` merged: bool | None = None ``` Whether merged. ### verified ``` verified: bool | None = None ``` Whether verified. ### tags ``` tags: list[str] | None = None ``` Tag names. ### contacts ``` contacts: list[str] | None = None ``` Contact emails. ### team_contacts ``` team_contacts: list[str] | None = None ``` Team contact emails. ## mixpanel_headless.BulkUpdateEventsParams Bases: `BaseModel` Parameters for bulk-updating event definitions. | ATTRIBUTE | DESCRIPTION | | --------- | -------------------------------------------------------------------------- | | `events` | List of event update entries (required). **TYPE:** `list[BulkEventUpdate]` | Example ``` params = BulkUpdateEventsParams( events=[BulkEventUpdate(name="E1", hidden=True)] ) ``` ### events ``` events: list[BulkEventUpdate] ``` List of event update entries. ## Property Definition Types ## mixpanel_headless.PropertyDefinition Bases: `BaseModel` A Mixpanel property definition from the Lexicon. | ATTRIBUTE | DESCRIPTION | | --------------- | ------------------------------------------------------------------- | | `id` | Server-assigned property ID. **TYPE:** \`int | | `name` | Property name. **TYPE:** `str` | | `resource_type` | Property resource type (event, user, groupprofile). **TYPE:** \`str | | `description` | Property description. **TYPE:** \`str | | `hidden` | Whether hidden from UI. **TYPE:** \`bool | | `dropped` | Whether data is dropped. **TYPE:** \`bool | | `merged` | Whether merged into another property. **TYPE:** \`bool | | `sensitive` | PII flag. **TYPE:** \`bool | | `data_group_id` | Data group identifier. **TYPE:** \`str | Example ``` prop = PropertyDefinition(id=1, name="$browser") ``` ### id ``` id: int | None = None ``` Server-assigned property ID (may be absent for custom properties). ### name ``` name: str ``` Property name. ### resource_type ``` resource_type: str | None = None ``` Property resource type (event, user, groupprofile). ### description ``` description: str | None = None ``` Property description. ### hidden ``` hidden: bool | None = None ``` Whether hidden from UI. ### dropped ``` dropped: bool | None = None ``` Whether data is dropped. ### merged ``` merged: bool | None = None ``` Whether merged into another property. ### sensitive ``` sensitive: bool | None = None ``` PII flag. ### data_group_id ``` data_group_id: str | None = None ``` Data group identifier. ## mixpanel_headless.UpdatePropertyDefinitionParams Bases: `BaseModel` Parameters for updating a property definition (PATCH semantics). All fields are optional; only set fields are sent. | ATTRIBUTE | DESCRIPTION | | ------------- | ----------------------------------------- | | `hidden` | Whether hidden from UI. **TYPE:** \`bool | | `dropped` | Whether data is dropped. **TYPE:** \`bool | | `merged` | Whether merged. **TYPE:** \`bool | | `sensitive` | PII flag. **TYPE:** \`bool | | `description` | Property description. **TYPE:** \`str | Example ``` params = UpdatePropertyDefinitionParams(sensitive=True) ``` ### hidden ``` hidden: bool | None = None ``` Whether hidden from UI. ### dropped ``` dropped: bool | None = None ``` Whether data is dropped. ### merged ``` merged: bool | None = None ``` Whether merged. ### sensitive ``` sensitive: bool | None = None ``` PII flag. ### description ``` description: str | None = None ``` Property description. ## mixpanel_headless.BulkPropertyUpdate Bases: `BaseModel` A single property update entry for bulk operations. Uses camelCase serialization to match the Django API contract. | ATTRIBUTE | DESCRIPTION | | --------------- | ----------------------------------------- | | `name` | Property name (required). **TYPE:** `str` | | `resource_type` | Resource type (required). **TYPE:** `str` | | `id` | Property ID. **TYPE:** \`int | | `hidden` | Whether hidden from UI. **TYPE:** \`bool | | `dropped` | Whether data is dropped. **TYPE:** \`bool | | `sensitive` | PII flag. **TYPE:** \`bool | | `data_group_id` | Data group identifier. **TYPE:** \`str | Example ``` entry = BulkPropertyUpdate(name="$browser", resource_type="event") ``` ### name ``` name: str ``` Property name. ### resource_type ``` resource_type: str ``` Resource type (event, user, groupprofile). ### id ``` id: int | None = None ``` Property ID. ### hidden ``` hidden: bool | None = None ``` Whether hidden from UI. ### dropped ``` dropped: bool | None = None ``` Whether data is dropped. ### sensitive ``` sensitive: bool | None = None ``` PII flag. ### data_group_id ``` data_group_id: str | None = None ``` Data group identifier. ## mixpanel_headless.BulkUpdatePropertiesParams Bases: `BaseModel` Parameters for bulk-updating property definitions. | ATTRIBUTE | DESCRIPTION | | ------------ | -------------------------------------------------------------------------------- | | `properties` | List of property update entries (required). **TYPE:** `list[BulkPropertyUpdate]` | Example ``` params = BulkUpdatePropertiesParams( properties=[BulkPropertyUpdate(name="$browser", resource_type="event")] ) ``` ### properties ``` properties: list[BulkPropertyUpdate] ``` List of property update entries. ## Lexicon Tag Types ## mixpanel_headless.LexiconTag Bases: `BaseModel` A Lexicon tag for categorizing event/property definitions. | ATTRIBUTE | DESCRIPTION | | --------- | --------------------------------------- | | `id` | Server-assigned tag ID. **TYPE:** `int` | | `name` | Tag name. **TYPE:** `str` | Example ``` tag = LexiconTag(id=1, name="core-metrics") ``` ### id ``` id: int ``` Server-assigned tag ID. ### name ``` name: str ``` Tag name. ## mixpanel_headless.CreateTagParams Bases: `BaseModel` Parameters for creating a Lexicon tag. | ATTRIBUTE | DESCRIPTION | | --------- | ----------------------------------------------- | | `name` | Tag name (required, non-empty). **TYPE:** `str` | Example ``` params = CreateTagParams(name="core-metrics") ``` ### name ``` name: str ``` Tag name. ## mixpanel_headless.UpdateTagParams Bases: `BaseModel` Parameters for updating a Lexicon tag. | ATTRIBUTE | DESCRIPTION | | --------- | ----------------------------- | | `name` | New tag name. **TYPE:** \`str | Example ``` params = UpdateTagParams(name="key-metrics") ``` ### name ``` name: str | None = None ``` New tag name. ## Drop Filter Types ## mixpanel_headless.DropFilter Bases: `BaseModel` A drop filter for discarding events at ingestion. | ATTRIBUTE | DESCRIPTION | | -------------- | ---------------------------------------------- | | `id` | Server-assigned filter ID. **TYPE:** `int` | | `event_name` | Event name to filter. **TYPE:** `str` | | `filters` | Filter condition JSON. **TYPE:** \`list[Any] | | `active` | Whether the filter is active. **TYPE:** \`bool | | `display_name` | Human-readable name. **TYPE:** \`str | | `created` | ISO 8601 creation timestamp. **TYPE:** \`str | Example ``` df = DropFilter(id=1, event_name="debug_log") ``` ### id ``` id: int ``` Server-assigned filter ID. ### event_name ``` event_name: str ``` Event name to filter. ### filters ``` filters: list[Any] | None = None ``` Filter condition JSON. ### active ``` active: bool | None = None ``` Whether the filter is active. ### display_name ``` display_name: str | None = None ``` Human-readable name. ### created ``` created: str | None = None ``` ISO 8601 creation timestamp. ## mixpanel_headless.CreateDropFilterParams Bases: `BaseModel` Parameters for creating a drop filter. | ATTRIBUTE | DESCRIPTION | | ------------ | ------------------------------------------------- | | `event_name` | Event name to filter (required). **TYPE:** `str` | | `filters` | Filter condition JSON (required). **TYPE:** `Any` | Example ``` params = CreateDropFilterParams( event_name="debug_log", filters={"property": "env", "operator": "equals", "value": "test"}, ) ``` ### event_name ``` event_name: str ``` Event name to filter. ### filters ``` filters: Any ``` Filter condition JSON. ## mixpanel_headless.UpdateDropFilterParams Bases: `BaseModel` Parameters for updating a drop filter. | ATTRIBUTE | DESCRIPTION | | ------------ | ---------------------------------------------- | | `id` | Drop filter ID (required). **TYPE:** `int` | | `event_name` | New event name. **TYPE:** \`str | | `filters` | New filter condition JSON. **TYPE:** \`Any | | `active` | Whether the filter is active. **TYPE:** \`bool | Example ``` params = UpdateDropFilterParams(id=123, active=False) ``` ### id ``` id: int ``` Drop filter ID. ### event_name ``` event_name: str | None = None ``` New event name. ### filters ``` filters: Any | None = None ``` New filter condition JSON. ### active ``` active: bool | None = None ``` Whether the filter is active. ## mixpanel_headless.DropFilterLimitsResponse Bases: `BaseModel` Response model for drop filter limits. | ATTRIBUTE | DESCRIPTION | | -------------- | ---------------------------------------- | | `filter_limit` | Maximum allowed filters. **TYPE:** `int` | Example ``` limits = DropFilterLimitsResponse(filter_limit=10) ``` ### filter_limit ``` filter_limit: int ``` Maximum allowed filters. ## Custom Property Types ## mixpanel_headless.ComposedPropertyValue Bases: `BaseModel` A composed property reference within a custom property formula. | ATTRIBUTE | DESCRIPTION | | -------------------- | ----------------------------------------- | | `type` | Property type. **TYPE:** \`str | | `type_cast` | Type cast instruction. **TYPE:** \`str | | `resource_type` | Resource type (required). **TYPE:** `str` | | `behavior` | Behavior specification. **TYPE:** \`Any | | `join_property_type` | Join property type. **TYPE:** \`str | Example ``` cpv = ComposedPropertyValue(resource_type="event") ``` ### type ``` type: str | None = None ``` Property type. ### type_cast ``` type_cast: str | None = None ``` Type cast instruction. ### resource_type ``` resource_type: str ``` Resource type. Uses singular form (event, user, groupprofile) from the Mixpanel API composed property schema β€” distinct from `CustomPropertyResourceType` which uses plural form. ### value ``` value: str | None = None ``` Property name in the project (e.g. `"deal_name"`). ### label ``` label: str | None = None ``` Human-readable label for the property (e.g. `"Deal Name"`). ### property_default_type ``` property_default_type: CustomPropertyType | None = None ``` Default property type hint (e.g. `"string"`, `"number"`). ### behavior ``` behavior: Any | None = None ``` Behavior specification. ### join_property_type ``` join_property_type: str | None = None ``` Join property type. ## mixpanel_headless.CustomProperty Bases: `BaseModel` A Mixpanel custom property (computed/formula property). | ATTRIBUTE | DESCRIPTION | | --------------------- | -------------------------------------------------------------------------------------- | | `custom_property_id` | Server-assigned property ID. **TYPE:** `int` | | `name` | Property name. **TYPE:** `str` | | `description` | Property description. **TYPE:** \`str | | `resource_type` | Resource type (events, people, group_profiles). **TYPE:** `CustomPropertyResourceType` | | `property_type` | Property type. **TYPE:** \`str | | `display_formula` | Formula expression. **TYPE:** \`str | | `composed_properties` | Referenced properties in formula. **TYPE:** \`dict[str, ComposedPropertyValue] | | `is_locked` | Whether the property is locked. **TYPE:** \`bool | | `is_visible` | Whether the property is visible. **TYPE:** \`bool | | `data_group_id` | Data group identifier. **TYPE:** \`str | | `created` | ISO 8601 creation timestamp. **TYPE:** \`str | | `modified` | ISO 8601 modification timestamp. **TYPE:** \`str | | `example_value` | Example value. **TYPE:** \`str | Example ``` cp = CustomProperty( custom_property_id=1, name="Revenue", resource_type="events" ) ``` ### custom_property_id ``` custom_property_id: int ``` Server-assigned property ID. ### name ``` name: str ``` Property name. ### description ``` description: str | None = None ``` Property description. ### resource_type ``` resource_type: CustomPropertyResourceType ``` Resource type (events, people, group_profiles). ### property_type ``` property_type: str | None = None ``` Property type. ### display_formula ``` display_formula: str | None = None ``` Formula expression. ### composed_properties ``` composed_properties: dict[str, ComposedPropertyValue] | None = None ``` Referenced properties in formula. ### is_locked ``` is_locked: bool | None = None ``` Whether the property is locked. ### is_visible ``` is_visible: bool | None = None ``` Whether the property is visible. ### data_group_id ``` data_group_id: str | None = None ``` Data group identifier. ### created ``` created: str | None = None ``` ISO 8601 creation timestamp. ### modified ``` modified: str | None = None ``` ISO 8601 modification timestamp. ### example_value ``` example_value: str | None = None ``` Example value. ## mixpanel_headless.CreateCustomPropertyParams Bases: `BaseModel` Parameters for creating a custom property. Validation rules: - `display_formula` and `behavior` are mutually exclusive. - `behavior` and `composed_properties` are mutually exclusive. - `display_formula` requires `composed_properties`. - One of `display_formula` or `behavior` must be set. | ATTRIBUTE | DESCRIPTION | | --------------------- | ----------------------------------------------------------------------------------------------------- | | `name` | Property name (required). **TYPE:** `str` | | `resource_type` | Resource type (required). **TYPE:** `CustomPropertyResourceType` | | `description` | Property description. **TYPE:** \`str | | `display_formula` | Formula expression (mutually exclusive with behavior). **TYPE:** \`str | | `composed_properties` | Referenced properties (required if display_formula set). **TYPE:** \`dict[str, ComposedPropertyValue] | | `is_locked` | Whether the property is locked. **TYPE:** \`bool | | `is_visible` | Whether the property is visible. **TYPE:** \`bool | | `data_group_id` | Data group identifier. **TYPE:** \`str | | `behavior` | Behavior specification (mutually exclusive with display_formula). **TYPE:** \`Any | Example ``` params = CreateCustomPropertyParams( name="Revenue Per User", resource_type="events", display_formula='number(properties["amount"])', composed_properties={"amount": ComposedPropertyValue(resource_type="event")}, ) ``` ### name ``` name: str ``` Property name. ### resource_type ``` resource_type: CustomPropertyResourceType ``` Resource type (events, people, group_profiles). ### description ``` description: str | None = None ``` Property description. ### display_formula ``` display_formula: str | None = None ``` Formula expression (mutually exclusive with behavior). ### composed_properties ``` composed_properties: dict[str, ComposedPropertyValue] | None = None ``` Referenced properties (required if display_formula set). ### is_locked ``` is_locked: bool | None = None ``` Whether the property is locked. ### is_visible ``` is_visible: bool | None = None ``` Whether the property is visible. ### property_type ``` property_type: CustomPropertyType | None = None ``` Output type of the custom property (string, number, boolean, datetime). Auto-inferred by the API from the formula if not set. ### example_value ``` example_value: str | None = None ``` Example output value for documentation purposes. ### data_group_id ``` data_group_id: str | None = None ``` Data group identifier. ### behavior ``` behavior: Any | None = None ``` Behavior specification (mutually exclusive with display_formula). ## mixpanel_headless.UpdateCustomPropertyParams Bases: `BaseModel` Parameters for updating a custom property (PUT β€” full replacement). Note: `resource_type` and `data_group_id` are immutable. | ATTRIBUTE | DESCRIPTION | | --------------------- | ------------------------------------------------------------------- | | `name` | Property name. **TYPE:** \`str | | `description` | Property description. **TYPE:** \`str | | `display_formula` | Formula expression. **TYPE:** \`str | | `composed_properties` | Referenced properties. **TYPE:** \`dict[str, ComposedPropertyValue] | | `is_locked` | Whether the property is locked. **TYPE:** \`bool | | `is_visible` | Whether the property is visible. **TYPE:** \`bool | Example ``` params = UpdateCustomPropertyParams(name="Updated Name") ``` ### name ``` name: str | None = None ``` Property name. ### description ``` description: str | None = None ``` Property description. ### display_formula ``` display_formula: str | None = None ``` Formula expression. ### composed_properties ``` composed_properties: dict[str, ComposedPropertyValue] | None = None ``` Referenced properties. ### is_locked ``` is_locked: bool | None = None ``` Whether the property is locked. ### is_visible ``` is_visible: bool | None = None ``` Whether the property is visible. ## Lookup Table Types ## mixpanel_headless.LookupTable Bases: `BaseModel` A Mixpanel lookup table. | ATTRIBUTE | DESCRIPTION | | ----------------------- | --------------------------------------------------------- | | `id` | Server-assigned table ID. **TYPE:** `int` | | `name` | Table name. **TYPE:** `str` | | `token` | Table token. **TYPE:** \`str | | `created_at` | ISO 8601 creation timestamp. **TYPE:** \`str | | `last_modified_at` | ISO 8601 modification timestamp. **TYPE:** \`str | | `has_mapped_properties` | Whether the table has mapped properties. **TYPE:** \`bool | Example ``` lt = LookupTable(id=1, name="Product Catalog") ``` ### id ``` id: int ``` Server-assigned table ID. ### name ``` name: str ``` Table name. ### token ``` token: str | None = None ``` Table token. ### created_at ``` created_at: str | None = None ``` ISO 8601 creation timestamp. ### last_modified_at ``` last_modified_at: str | None = None ``` ISO 8601 modification timestamp. ### has_mapped_properties ``` has_mapped_properties: bool | None = None ``` Whether the table has mapped properties. ## mixpanel_headless.UploadLookupTableParams Bases: `BaseModel` Parameters for uploading a lookup table CSV. The upload is a 3-step process handled by the workspace method: 1. Get a signed upload URL 1. Upload CSV to signed URL 1. Register the table | ATTRIBUTE | DESCRIPTION | | --------------- | -------------------------------------------------------- | | `name` | Table name (1-255 characters, required). **TYPE:** `str` | | `file_path` | Path to local CSV file (required). **TYPE:** `str` | | `data_group_id` | For replacing an existing table. **TYPE:** \`int | Example ``` params = UploadLookupTableParams( name="Product Catalog", file_path="/path/to/products.csv" ) ``` ### name ``` name: str = Field(min_length=1, max_length=255) ``` Table name (1-255 characters). ### file_path ``` file_path: str ``` Path to local CSV file. ### data_group_id ``` data_group_id: int | None = None ``` For replacing an existing table. ## mixpanel_headless.MarkLookupTableReadyParams Bases: `BaseModel` Parameters for marking a lookup table as ready. | ATTRIBUTE | DESCRIPTION | | --------------- | --------------------------------------------------- | | `name` | Table name (required). **TYPE:** `str` | | `key` | Primary key column name (required). **TYPE:** `str` | | `data_group_id` | For replacing an existing table. **TYPE:** \`int | Example ``` params = MarkLookupTableReadyParams(name="Products", key="product_id") ``` ### name ``` name: str ``` Table name. ### key ``` key: str ``` Primary key column name. ### data_group_id ``` data_group_id: int | None = None ``` For replacing an existing table. ## mixpanel_headless.LookupTableUploadUrl Bases: `BaseModel` Response model for lookup table upload URL request. | ATTRIBUTE | DESCRIPTION | | --------- | ------------------------------------------ | | `url` | Signed GCS upload URL. **TYPE:** `str` | | `path` | GCS path for registration. **TYPE:** `str` | | `key` | Primary key column name. **TYPE:** `str` | Example ``` upload = LookupTableUploadUrl( url="https://storage.googleapis.com/...", path="gs://bucket/path", key="id", ) ``` ### url ``` url: str ``` Signed GCS upload URL. ### path ``` path: str ``` GCS path for registration. ### key ``` key: str ``` Primary key column name. ## mixpanel_headless.UpdateLookupTableParams Bases: `BaseModel` Parameters for updating a lookup table. | ATTRIBUTE | DESCRIPTION | | --------- | ------------------------------- | | `name` | New table name. **TYPE:** \`str | Example ``` params = UpdateLookupTableParams(name="Updated Catalog") ``` ### name ``` name: str | None = None ``` New table name. ## Schema Registry Types Types for managing JSON Schema Draft 7 definitions in the schema registry. ## mixpanel_headless.SchemaEntry Bases: `BaseModel` A schema registry entry for an event, custom event, or profile. Represents a JSON Schema Draft 7 definition registered in the Mixpanel schema registry. Used for both API responses and as entries in bulk create/update operations. | ATTRIBUTE | DESCRIPTION | | ------------------- | ---------------------------------------------------------------------------------- | | `entity_type` | Entity type ("event", "custom_event", "profile"). **TYPE:** `str` | | `name` | Entity name (event name or "$user" for profile). **TYPE:** `str` | | `version` | Schema version in YYYY-MM-DD format. **TYPE:** \`str | | `schema_definition` | JSON Schema Draft 7 definition (API field: schemaJson). **TYPE:** `dict[str, Any]` | Example ``` entry = SchemaEntry( entity_type="event", name="Purchase", schema_definition={"properties": {"amount": {"type": "number"}}}, ) # Or using the API alias: entry = SchemaEntry( entityType="event", name="Purchase", schemaJson={"properties": {"amount": {"type": "number"}}}, ) ``` ### entity_type ``` entity_type: str ``` Entity type: "event", "custom_event", or "profile". ### name ``` name: str ``` Entity name (event name or "$user" for profile). ### version ``` version: str | None = None ``` Schema version in YYYY-MM-DD format. ### schema_definition ``` schema_definition: dict[str, Any] = Field(alias='schemaJson') ``` JSON Schema Draft 7 definition (API field: schemaJson). ## mixpanel_headless.BulkCreateSchemasParams Bases: `BaseModel` Parameters for bulk-creating schemas in the registry. | ATTRIBUTE | DESCRIPTION | | ------------- | ------------------------------------------------------------------------------------------ | | `entries` | Schema entries to create. **TYPE:** `list[SchemaEntry]` | | `truncate` | If true, delete all existing schemas of entity_type before inserting. **TYPE:** \`bool | | `entity_type` | Entity type for all entries (only "event" supported for batch operations). **TYPE:** \`str | Example ``` params = BulkCreateSchemasParams( entries=[ SchemaEntry(name="Login", entity_type="event", schema_definition={...}), ], truncate=True, entity_type="event", ) ``` ### entries ``` entries: list[SchemaEntry] ``` Schema entries to create. ### truncate ``` truncate: bool | None = None ``` If true, delete all existing schemas of entity_type before inserting. ### entity_type ``` entity_type: str | None = None ``` Entity type for all entries (only "event" supported for batch). ## mixpanel_headless.BulkCreateSchemasResponse Bases: `BaseModel` Response from a bulk schema creation operation. | ATTRIBUTE | DESCRIPTION | | --------- | ---------------------------------------------------------- | | `added` | Number of schemas added. **TYPE:** `int` | | `deleted` | Number of schemas deleted (from truncate). **TYPE:** `int` | Example ``` resp = BulkCreateSchemasResponse(added=5, deleted=3) ``` ### added ``` added: int ``` Number of schemas added. ### deleted ``` deleted: int ``` Number of schemas deleted (from truncate). ## mixpanel_headless.BulkPatchResult Bases: `BaseModel` Per-entry result from a bulk schema update operation. | ATTRIBUTE | DESCRIPTION | | ------------- | --------------------------------------------------- | | `entity_type` | Entity type processed. **TYPE:** `str` | | `name` | Entity name processed. **TYPE:** `str` | | `status` | Result status ("ok" or "error"). **TYPE:** `str` | | `error` | Error message if status is "error". **TYPE:** \`str | Example ``` result = BulkPatchResult( entity_type="event", name="Login", status="ok" ) ``` ### entity_type ``` entity_type: str ``` Entity type processed. ### name ``` name: str ``` Entity name processed. ### status ``` status: str ``` Result status ("ok" or "error"). ### error ``` error: str | None = None ``` Error message if status is "error". ## mixpanel_headless.DeleteSchemasResponse Bases: `BaseModel` Response from a schema deletion operation. | ATTRIBUTE | DESCRIPTION | | -------------- | ------------------------------------------ | | `delete_count` | Number of schemas deleted. **TYPE:** `int` | Example ``` resp = DeleteSchemasResponse(delete_count=3) ``` ### delete_count ``` delete_count: int ``` Number of schemas deleted. ## Schema Enforcement Types Types for configuring schema enforcement policies. ## mixpanel_headless.SchemaEnforcementConfig Bases: `BaseModel` Schema enforcement configuration for a project. Controls how Mixpanel handles events that don't match defined schemas. | ATTRIBUTE | DESCRIPTION | | --------------------- | ----------------------------------------------------------------------------------------- | | `id` | Config ID. **TYPE:** \`int | | `last_modified` | Last modification timestamp. **TYPE:** \`str | | `last_modified_by` | User who last modified. **TYPE:** \`dict[str, Any] | | `rule_event` | Enforcement action ("Warn and Accept", "Warn and Hide", "Warn and Drop"). **TYPE:** \`str | | `notification_emails` | Notification recipients. **TYPE:** \`list[str] | | `events` | Event enforcement rules. **TYPE:** \`list\[dict[str, Any]\] | | `common_properties` | Common property rules. **TYPE:** \`list\[dict[str, Any]\] | | `user_properties` | User property rules. **TYPE:** \`list\[dict[str, Any]\] | | `initialized_by` | User who initialized. **TYPE:** \`dict[str, Any] | | `initialized_from` | Initialization start date. **TYPE:** \`str | | `initialized_to` | Initialization end date. **TYPE:** \`str | | `state` | Enforcement state ("planned" or "ingested"). **TYPE:** \`str | Example ``` config = SchemaEnforcementConfig( id=1, rule_event="Warn and Accept", state="ingested" ) ``` ### id ``` id: int | None = None ``` Config ID. ### last_modified ``` last_modified: str | None = None ``` Last modification timestamp. ### last_modified_by ``` last_modified_by: dict[str, Any] | None = None ``` User who last modified. ### rule_event ``` rule_event: str | None = None ``` Enforcement action: "Warn and Accept", "Warn and Hide", "Warn and Drop". ### notification_emails ``` notification_emails: list[str] | None = None ``` Notification recipients. ### events ``` events: list[dict[str, Any]] | None = None ``` Event enforcement rules. ### common_properties ``` common_properties: list[dict[str, Any]] | None = None ``` Common property rules. ### user_properties ``` user_properties: list[dict[str, Any]] | None = None ``` User property rules. ### initialized_by ``` initialized_by: dict[str, Any] | None = None ``` User who initialized. ### initialized_from ``` initialized_from: str | None = None ``` Initialization start date. ### initialized_to ``` initialized_to: str | None = None ``` Initialization end date. ### state ``` state: str | None = None ``` Enforcement state ("planned" or "ingested"). ## mixpanel_headless.InitSchemaEnforcementParams Bases: `BaseModel` Parameters for initializing schema enforcement. | ATTRIBUTE | DESCRIPTION | | ------------ | ----------------------------------------------------------------------------------------- | | `rule_event` | Enforcement action ("Warn and Accept", "Warn and Hide", "Warn and Drop"). **TYPE:** `str` | Example ``` params = InitSchemaEnforcementParams(rule_event="Warn and Accept") ``` ### rule_event ``` rule_event: str ``` Enforcement action. ## mixpanel_headless.UpdateSchemaEnforcementParams Bases: `BaseModel` Parameters for partially updating schema enforcement. | ATTRIBUTE | DESCRIPTION | | --------------------- | -------------------------------------------------------- | | `notification_emails` | Updated notification recipients. **TYPE:** \`list[str] | | `rule_event` | Updated enforcement action. **TYPE:** \`str | | `events` | Updated event list. **TYPE:** \`list[str] | | `properties` | Updated property map. **TYPE:** \`dict\[str, list[str]\] | Example ``` params = UpdateSchemaEnforcementParams( rule_event="Warn and Drop", notification_emails=["data-team@example.com"], ) ``` ### notification_emails ``` notification_emails: list[str] | None = None ``` Updated notification recipients. ### rule_event ``` rule_event: str | None = None ``` Updated enforcement action. ### events ``` events: list[str] | None = None ``` Updated event list. ### properties ``` properties: dict[str, list[str]] | None = None ``` Updated property map. ## mixpanel_headless.ReplaceSchemaEnforcementParams Bases: `BaseModel` Parameters for fully replacing schema enforcement configuration. All fields are required since this is a full replacement. | ATTRIBUTE | DESCRIPTION | | --------------------- | ------------------------------------------------------------ | | `common_properties` | Full common property rules. **TYPE:** `list[dict[str, Any]]` | | `user_properties` | Full user property rules. **TYPE:** `list[dict[str, Any]]` | | `events` | Full event rules. **TYPE:** `list[dict[str, Any]]` | | `rule_event` | Enforcement action. **TYPE:** `str` | | `notification_emails` | Notification recipients. **TYPE:** `list[str]` | | `schema_id` | Schema definition ID. **TYPE:** \`int | Example ``` params = ReplaceSchemaEnforcementParams( events=[...], common_properties=[...], user_properties=[...], rule_event="Warn and Hide", notification_emails=["admin@example.com"], ) ``` ### common_properties ``` common_properties: list[dict[str, Any]] ``` Full common property rules. ### user_properties ``` user_properties: list[dict[str, Any]] ``` Full user property rules. ### events ``` events: list[dict[str, Any]] ``` Full event rules. ### rule_event ``` rule_event: str ``` Enforcement action. ### notification_emails ``` notification_emails: list[str] ``` Notification recipients. ### schema_id ``` schema_id: int | None = None ``` Schema definition ID. ## Data Audit Types Types for schema audit operations and violation reporting. ## mixpanel_headless.AuditViolation Bases: `BaseModel` A single violation found during a data audit. | ATTRIBUTE | DESCRIPTION | | --------------------- | -------------------------------------------------------------------------------------------------------------- | | `violation` | Violation type (e.g., "Unexpected Event", "Missing Property", "Unexpected Type for Property"). **TYPE:** `str` | | `name` | Property or event name. **TYPE:** `str` | | `platform` | Platform ("iOS", "Android", "Web"). **TYPE:** \`str | | `version` | Version string. **TYPE:** \`str | | `count` | Number of occurrences. **TYPE:** `int` | | `event` | Event name (for property violations). **TYPE:** \`str | | `sensitive` | Whether property is marked sensitive. **TYPE:** \`bool | | `property_type_error` | Type mismatch description. **TYPE:** \`str | Example ``` v = AuditViolation( violation="Unexpected Event", name="DebugLog", count=42 ) ``` ### violation ``` violation: str ``` Violation type. ### name ``` name: str ``` Property or event name. ### platform ``` platform: str | None = None ``` Platform: "iOS", "Android", "Web". ### version ``` version: str | None = None ``` Version string. ### count ``` count: int ``` Number of occurrences. ### event ``` event: str | None = None ``` Event name (for property violations). ### sensitive ``` sensitive: bool | None = None ``` Whether property is marked sensitive. ### property_type_error ``` property_type_error: str | None = None ``` Type mismatch description. ## mixpanel_headless.AuditResponse Bases: `BaseModel` Response from a data audit operation. Contains a list of schema violations and the timestamp when the audit was computed. | ATTRIBUTE | DESCRIPTION | | ------------- | ---------------------------------------------------------- | | `violations` | List of audit violations. **TYPE:** `list[AuditViolation]` | | `computed_at` | Timestamp of audit computation. **TYPE:** `str` | Example ``` resp = AuditResponse( violations=[ AuditViolation(violation="Unexpected Event", name="Debug", count=1) ], computed_at="2026-04-01T12:00:00Z", ) ``` ### violations ``` violations: list[AuditViolation] ``` List of audit violations. ### computed_at ``` computed_at: str ``` Timestamp of audit computation. ## Data Volume Anomaly Types Types for monitoring and managing data volume anomalies. ## mixpanel_headless.DataVolumeAnomaly Bases: `BaseModel` A detected data volume anomaly. | ATTRIBUTE | DESCRIPTION | | ------------------ | ----------------------------------------------------------------------------------- | | `id` | Anomaly ID. **TYPE:** `int` | | `timestamp` | Detection timestamp. **TYPE:** \`str | | `actual_count` | Actual observed count. **TYPE:** `int` | | `predicted_upper` | Upper bound of prediction. **TYPE:** `int` | | `predicted_lower` | Lower bound of prediction. **TYPE:** `int` | | `percent_variance` | Variance percentage. **TYPE:** `str` | | `status` | Anomaly status ("open" or "dismissed"). **TYPE:** `str` | | `project` | Project ID. **TYPE:** `int` | | `event` | Event ID. **TYPE:** \`int | | `event_name` | Event name. **TYPE:** \`str | | `property` | Property ID. **TYPE:** \`int | | `property_name` | Property name. **TYPE:** \`str | | `metric` | Metric ID. **TYPE:** \`int | | `metric_name` | Metric name. **TYPE:** \`str | | `metric_type` | Metric type. **TYPE:** \`str | | `primary_type` | Primary anomaly type. **TYPE:** \`str | | `drift_types` | Drift type details. **TYPE:** \`dict[str, Any] | | `anomaly_class` | Anomaly class ("Event", "Property", "PropertyTypeDrift", "Metric"). **TYPE:** `str` | Example ``` anomaly = DataVolumeAnomaly( id=1, actual_count=1000, predicted_upper=500, predicted_lower=100, percent_variance="100%", status="open", project=12345, anomaly_class="Event", ) ``` ### id ``` id: int ``` Anomaly ID. ### timestamp ``` timestamp: str | None = None ``` Detection timestamp. ### actual_count ``` actual_count: int ``` Actual observed count. ### predicted_upper ``` predicted_upper: int ``` Upper bound of prediction. ### predicted_lower ``` predicted_lower: int ``` Lower bound of prediction. ### percent_variance ``` percent_variance: str ``` Variance percentage. ### status ``` status: str ``` Anomaly status ("open" or "dismissed"). ### project ``` project: int ``` Project ID. ### event ``` event: int | None = None ``` Event ID. ### event_name ``` event_name: str | None = None ``` Event name. ### property ``` property: int | None = None ``` Property ID. ### property_name ``` property_name: str | None = None ``` Property name. ### metric ``` metric: int | None = None ``` Metric ID. ### metric_name ``` metric_name: str | None = None ``` Metric name. ### metric_type ``` metric_type: str | None = None ``` Metric type. ### primary_type ``` primary_type: str | None = None ``` Primary anomaly type. ### drift_types ``` drift_types: dict[str, Any] | None = None ``` Drift type details. ### anomaly_class ``` anomaly_class: str ``` Anomaly class: "Event", "Property", "PropertyTypeDrift", "Metric". ## mixpanel_headless.UpdateAnomalyParams Bases: `BaseModel` Parameters for updating a single anomaly status. | ATTRIBUTE | DESCRIPTION | | --------------- | --------------------------------------------------- | | `id` | Anomaly ID. **TYPE:** `int` | | `status` | New status ("open" or "dismissed"). **TYPE:** `str` | | `anomaly_class` | Anomaly class. **TYPE:** `str` | Example ``` params = UpdateAnomalyParams( id=123, status="dismissed", anomaly_class="Event" ) ``` ### id ``` id: int ``` Anomaly ID. ### status ``` status: str ``` New status: "open" or "dismissed". ### anomaly_class ``` anomaly_class: str ``` Anomaly class. ## mixpanel_headless.BulkAnomalyEntry Bases: `BaseModel` A single entry in a bulk anomaly update. | ATTRIBUTE | DESCRIPTION | | --------------- | ------------------------------ | | `id` | Anomaly ID. **TYPE:** `int` | | `anomaly_class` | Anomaly class. **TYPE:** `str` | Example ``` entry = BulkAnomalyEntry(id=123, anomaly_class="Event") ``` ### id ``` id: int ``` Anomaly ID. ### anomaly_class ``` anomaly_class: str ``` Anomaly class. ## mixpanel_headless.BulkUpdateAnomalyParams Bases: `BaseModel` Parameters for bulk-updating anomaly statuses. | ATTRIBUTE | DESCRIPTION | | ----------- | ----------------------------------------------------------- | | `anomalies` | Anomalies to update. **TYPE:** `list[BulkAnomalyEntry]` | | `status` | New status for all ("open" or "dismissed"). **TYPE:** `str` | Example ``` params = BulkUpdateAnomalyParams( anomalies=[BulkAnomalyEntry(id=1, anomaly_class="Event")], status="dismissed", ) ``` ### anomalies ``` anomalies: list[BulkAnomalyEntry] ``` Anomalies to update. ### status ``` status: str ``` New status for all. ## Event Deletion Request Types Types for managing event deletion requests. ## mixpanel_headless.EventDeletionRequest Bases: `BaseModel` An event deletion request with lifecycle status. | ATTRIBUTE | DESCRIPTION | | ---------------------- | ---------------------------------------------------------------------------------- | | `id` | Request ID. **TYPE:** `int` | | `display_name` | Display name. **TYPE:** \`str | | `event_name` | Event to delete. **TYPE:** `str` | | `from_date` | Start date. **TYPE:** `str` | | `to_date` | End date. **TYPE:** `str` | | `filters` | Deletion filters. **TYPE:** \`dict[str, Any] | | `status` | Request status ("Submitted", "Processing", "Completed", "Failed"). **TYPE:** `str` | | `deleted_events_count` | Count of deleted events. **TYPE:** `int` | | `created` | Creation timestamp. **TYPE:** `str` | | `requesting_user` | User who requested. **TYPE:** `dict[str, Any]` | Example ``` req = EventDeletionRequest( id=1, event_name="Test", from_date="2026-01-01", to_date="2026-01-31", status="Submitted", deleted_events_count=0, created="2026-04-01", requesting_user={"id": 1}, ) ``` ### id ``` id: int ``` Request ID. ### display_name ``` display_name: str | None = None ``` Display name. ### event_name ``` event_name: str ``` Event to delete. ### from_date ``` from_date: str ``` Start date. ### to_date ``` to_date: str ``` End date. ### filters ``` filters: dict[str, Any] | None = None ``` Deletion filters (dict when populated, None when absent). ### status ``` status: str ``` Request status: "Submitted", "Processing", "Completed", "Failed". ### deleted_events_count ``` deleted_events_count: int ``` Count of deleted events. ### created ``` created: str ``` Creation timestamp. ### requesting_user ``` requesting_user: dict[str, Any] ``` User who requested. ## mixpanel_headless.CreateDeletionRequestParams Bases: `BaseModel` Parameters for creating an event deletion request. | ATTRIBUTE | DESCRIPTION | | ------------ | ----------------------------------------------------- | | `from_date` | Start date (YYYY-MM-DD or datetime). **TYPE:** `str` | | `to_date` | End date. **TYPE:** `str` | | `event_name` | Event name to delete. **TYPE:** `str` | | `filters` | Optional deletion filters. **TYPE:** \`dict[str, Any] | Example ``` params = CreateDeletionRequestParams( event_name="Test Event", from_date="2026-01-01", to_date="2026-01-31", ) ``` ### from_date ``` from_date: str ``` Start date (YYYY-MM-DD or datetime). ### to_date ``` to_date: str ``` End date. ### event_name ``` event_name: str ``` Event name to delete. ### filters ``` filters: dict[str, Any] | None = None ``` Optional deletion filters. ## mixpanel_headless.PreviewDeletionFiltersParams Bases: `BaseModel` Parameters for previewing event deletion filters. This is a read-only operation that shows what events would match. | ATTRIBUTE | DESCRIPTION | | ------------ | -------------------------------------------- | | `event_name` | Event name. **TYPE:** `str` | | `from_date` | Start date. **TYPE:** `str` | | `to_date` | End date. **TYPE:** `str` | | `filters` | Optional filters. **TYPE:** \`dict[str, Any] | Example ``` params = PreviewDeletionFiltersParams( event_name="Test Event", from_date="2026-01-01", to_date="2026-01-31", ) ``` ### event_name ``` event_name: str ``` Event name. ### from_date ``` from_date: str ``` Start date. ### to_date ``` to_date: str ``` End date. ### filters ``` filters: dict[str, Any] | None = None ``` Optional filters. ## Business Context Types Types for the markdown documentation that grounds AI assistants β€” see the [Business Context guide](https://mixpanel.github.io/mixpanel-headless/guide/business-context/index.md). Both org and project scopes return the same `BusinessContext` model; `BusinessContextChain` bundles both for the convenience `get_business_context_chain()` round-trip. The 50,000-character cap is exposed as the constant `mixpanel_headless.BUSINESS_CONTEXT_MAX_CHARS` and enforced both client-side (before any HTTP call) and server-side. ## mixpanel_headless.BusinessContext Bases: `BaseModel` Business context content at a single scope. Returned by `Workspace.get_business_context()` and `Workspace.set_business_context()`. The `organization_id` field is populated for `level="organization"` and `project_id` for `level="project"` so callers can identify which scope a value came from when handling both in the same code path. | ATTRIBUTE | DESCRIPTION | | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | `level` | "organization" (org-wide) or "project" (project-specific). **TYPE:** `Literal['organization', 'project']` | | `content` | The markdown content. Empty string when no context is set at this scope. **TYPE:** `str` | | `organization_id` | Owning organization ID β€” populated when level="organization", None otherwise. **TYPE:** \`int | | `project_id` | Owning project ID β€” populated when level="project", None otherwise. **TYPE:** \`str | | `is_empty` | Computed β€” True when content == "". Visible in model_dump() output so JSON consumers can use it directly (no need to recompute). **TYPE:** `bool` | | `character_count` | Computed β€” len(content). Visible in model_dump() output. Compare against BUSINESS_CONTEXT_MAX_CHARS (50,000) to check headroom. **TYPE:** `int` | Example ``` ws = Workspace() ctx = ws.get_business_context(level="project") if not ctx.is_empty: print(ctx.content[:200], "...") print(f"({ctx.character_count} characters)") ``` ### level ``` level: Literal['organization', 'project'] ``` Which scope this context belongs to. ### content ``` content: str ``` Markdown content. Empty string when no context is set. ### organization_id ``` organization_id: int | None = None ``` Owning organization ID (set when `level="organization"`). ### project_id ``` project_id: str | None = None ``` Owning project ID (set when `level="project"`). ### is_empty ``` is_empty: bool ``` `True` when no content has been set at this scope. Computed field β€” appears in `model_dump()` so JSON / CLI consumers can `--jq '.is_empty'` directly. | RETURNS | DESCRIPTION | | ------- | ----------------------------------------------------- | | `bool` | True if content is the empty string, False otherwise. | ### character_count ``` character_count: int ``` Length of `content` in characters. Computed field β€” appears in `model_dump()` so JSON / CLI consumers can `--jq '.character_count'` directly. | RETURNS | DESCRIPTION | | ------- | -------------------------------------------------------- | | `int` | Number of Unicode characters in content. Compare against | | `int` | BUSINESS_CONTEXT_MAX_CHARS (50,000) to check headroom. | ## mixpanel_headless.BusinessContextChain Bases: `BaseModel` Both organization and project business context returned together. Returned by `Workspace.get_business_context_chain()`, which calls the project-scoped `/business-context/chain` endpoint and resolves both scopes in a single round-trip. | ATTRIBUTE | DESCRIPTION | | -------------- | ----------------------------------------------------------------------------------- | | `organization` | Organization-level context (shared across projects). **TYPE:** `BusinessContext` | | `project` | Project-level context (specific to the active project). **TYPE:** `BusinessContext` | Example ``` ws = Workspace() chain = ws.get_business_context_chain() print("ORG:", chain.organization.content) print("PROJECT:", chain.project.content) ``` ### organization ``` organization: BusinessContext ``` Organization-level context (`level="organization"`). ### project ``` project: BusinessContext ``` Project-level context (`level="project"`). Copy markdown # CLI Reference # CLI Overview The `mp` command provides full access to mixpanel_headless functionality from the command line. Explore on DeepWiki πŸ€– **[CLI Usage Guide β†’](https://deepwiki.com/mixpanel/mixpanel-headless/3.1-cli-usage)** Ask questions about CLI commands, explore options, or get help with specific workflows. ## Installation The CLI is installed automatically with the package: ``` pip install mixpanel_headless ``` Verify installation: ``` mp --version ``` ## Global Options | Option | Short | Description | | ------------- | ----- | -------------------------------------------------------------------------------- | | `--account` | `-a` | Account name to use (env: `MP_ACCOUNT`) | | `--project` | `-p` | Project ID override (env: `MP_PROJECT_ID`) | | `--workspace` | `-w` | Workspace ID override (env: `MP_WORKSPACE_ID`) | | `--target` | `-t` | Apply a saved target (env: `MP_TARGET`) β€” mutually exclusive with `-a`/`-p`/`-w` | | `--quiet` | `-q` | Suppress progress output | | `--verbose` | `-v` | Enable debug output | | `--version` | | Show version and exit | | `--help` | | Show help and exit | These resolve via the priority chain `env > param > target > bridge > [active] > default_project`. See [Configuration β†’ Credential Resolution Chain](https://mixpanel.github.io/mixpanel-headless/getting-started/configuration/#credential-resolution-chain). ## Command Groups ### login β€” Frictionless Authentication `mp login` is the recommended starter command. It composes auth-type detection, `/me` lookup, project picker, and account-name derivation into a single call: | Command | Description | | ---------- | ---------------------------------------------------------------------------------------- | | `mp login` | One-shot login (browser PKCE by default; auto-detects SA / static-bearer paths from env) | Useful flags: `--region {us\|eu\|in}` sets the region (required for EU / India when using the browser path; SA and oauth_token paths probe automatically when `--region` is omitted), `--project ID` skips the picker, `--name NAME` overrides the derived account name, `--service-account` / `--token-env VAR` force a non-browser path, `--no-browser` prints the authorization URL instead of launching a browser, `--secret-stdin` reads the SA secret from stdin. The auth-type detection ladder (in priority order): explicit `--service-account` / `--token-env` flag β†’ `MP_USERNAME` + `MP_SECRET` set β†’ `MP_OAUTH_TOKEN` set β†’ otherwise `oauth_browser`. Region behavior: SA and oauth_token paths probe `us β†’ eu β†’ in` when `--region` is omitted; the browser path defaults to `us`. See [Configuration β†’ Quick Start](https://mixpanel.github.io/mixpanel-headless/getting-started/configuration/#quick-start-mp-login) for examples. ### account β€” Account Management Manage configured accounts (service accounts, OAuth browser, OAuth token) and the active account. For first-time setup, prefer `mp login` above; the `account` group is for explicit registration, rotation, and lifecycle management. | Command | Description | | -------------------------- | ------------------------------------------------------------------------------------------------------- | | `mp account list` | List configured accounts (active marked with `*`) | | `mp account add` | Register a new account (`--type {service_account, oauth_browser, oauth_token}`) | | `mp account update` | Rotate region / secret / token / default_project on an existing account | | `mp account remove` | Delete an account (`--force` orphans dependent targets) | | `mp account use` | Switch the active account (clears workspace) | | `mp account show` | Display account metadata (omit name for active) | | `mp account test` | Probe `/me`; returns `AccountTestResult` | | `mp account login` | Run the OAuth browser PKCE flow (oauth_browser only) β€” `mp login --name NAME` is the unified equivalent | | `mp account logout` | Delete on-disk OAuth tokens (oauth_browser only) | | `mp account token` | Print the bearer token (for piping to curl etc.) | | `mp account export-bridge` | Write a v2 Cowork bridge file | | `mp account remove-bridge` | Delete the bridge file (idempotent) | ### project β€” Project Switching Discover and switch the active Mixpanel project (resolved against the active account's `/me` response). | Command | Description | | ----------------- | ----------------------------------------------------------------------------- | | `mp project list` | List accessible projects | | `mp project use` | Persist the active project (writes to the active account's `default_project`) | | `mp project show` | Show the active project | ### workspace β€” Workspace Switching Discover and switch the active workspace within the current project. | Command | Description | | ------------------- | ------------------------------------------------------------- | | `mp workspace list` | List workspaces for the current project | | `mp workspace use` | Persist the active workspace (writes to `[active].workspace`) | | `mp workspace show` | Show the active workspace | ### target β€” Saved Targets A target is a saved (account, project, optional workspace) bundle β€” a named cursor position you can apply with one command. | Command | Description | | ------------------ | ------------------------------------------------------------------------ | | `mp target list` | List configured targets | | `mp target add` | Register a new target (`--account A --project P [--workspace W]`) | | `mp target use` | Apply a target atomically (writes all three `[active]` axes in one save) | | `mp target show` | Show target details | | `mp target remove` | Delete a target | ### session β€” Active Session Inspection Show the resolved active session β€” account, project, workspace, user. | Command | Description | | --------------------- | ---------------------------------------------------------------- | | `mp session` | Print the active session (`-f json` for machine-readable output) | | `mp session --bridge` | Show bridge-resolved state (Cowork integration) | ### query β€” Query Operations Execute live analytics queries against the Mixpanel API. | Command | Description | | ------------------------------- | ------------------------------------------------- | | `mp query segmentation` | Time-series event counts | | `mp query funnel` | Funnel conversion analysis | | `mp query retention` | Cohort retention analysis | | `mp query jql` | Execute JQL scripts | | `mp query event-counts` | Multi-event time series | | `mp query property-counts` | Property breakdown time series | | `mp query activity-feed` | User event history | | `mp query saved-report` | Query saved reports (Insights, Retention, Funnel) | | `mp query flows` | Query saved Flows reports | | `mp query frequency` | Event frequency distribution | | `mp query segmentation-numeric` | Numeric property bucketing | | `mp query segmentation-sum` | Numeric property sum | | `mp query segmentation-average` | Numeric property average | Saved Reports Workflow Use `mp inspect bookmarks` to list available saved reports and get their IDs, then query them with `mp query saved-report` or `mp query flows`. ### inspect β€” Schema Discovery Explore your Mixpanel project schema. | Command | Description | | ---------------------------- | ----------------------------------------- | | `mp inspect events` | List event names | | `mp inspect properties` | List properties for an event | | `mp inspect values` | List values for a property | | `mp inspect funnels` | List saved funnels | | `mp inspect cohorts` | List saved cohorts | | `mp inspect bookmarks` | List saved reports (bookmarks) | | `mp inspect top-events` | List today's top events | | `mp inspect lexicon-schemas` | List Lexicon schemas from data dictionary | | `mp inspect lexicon-schema` | Get a single Lexicon schema | | `mp inspect distribution` | Property value distribution (JQL) | | `mp inspect numeric` | Numeric property statistics (JQL) | | `mp inspect daily` | Daily event counts (JQL) | | `mp inspect engagement` | User engagement distribution (JQL) | | `mp inspect coverage` | Property coverage analysis (JQL) | ### dashboards β€” Dashboard Management Manage Mixpanel dashboards via the App API. | Command | Description | | ---------------------------------- | ----------------------------------- | | `mp dashboards list` | List dashboards | | `mp dashboards create` | Create a new dashboard | | `mp dashboards get` | Get dashboard by ID | | `mp dashboards update` | Update a dashboard | | `mp dashboards delete` | Delete a dashboard | | `mp dashboards bulk-delete` | Delete multiple dashboards | | `mp dashboards favorite` | Favorite a dashboard | | `mp dashboards unfavorite` | Unfavorite a dashboard | | `mp dashboards pin` | Pin a dashboard | | `mp dashboards unpin` | Unpin a dashboard | | `mp dashboards add-report` | Add a report to a dashboard | | `mp dashboards remove-report` | Remove a report from a dashboard | | `mp dashboards blueprints` | List blueprint templates | | `mp dashboards blueprint-create` | Create dashboard from blueprint | | `mp dashboards rca` | Create RCA dashboard | | `mp dashboards erf` | Get dashboard ERF metrics | | `mp dashboards update-report-link` | Update a report link on a dashboard | | `mp dashboards update-text-card` | Update a text card on a dashboard | ### reports β€” Report Management Manage Mixpanel reports (bookmarks) via the App API. | Command | Description | | ------------------------------ | ------------------------------------------ | | `mp reports list` | List reports with optional type/ID filters | | `mp reports create` | Create a new report | | `mp reports get` | Get report by ID | | `mp reports update` | Update a report | | `mp reports delete` | Delete a report | | `mp reports bulk-delete` | Delete multiple reports | | `mp reports bulk-update` | Update multiple reports | | `mp reports linked-dashboards` | Get dashboards containing a report | | `mp reports dashboard-ids` | Get dashboard IDs for a report | | `mp reports history` | Get report change history | ### cohorts β€” Cohort Management Manage Mixpanel cohorts via the App API. | Command | Description | | ------------------------ | ---------------------------------- | | `mp cohorts list` | List cohorts with optional filters | | `mp cohorts create` | Create a new cohort | | `mp cohorts get` | Get cohort by ID | | `mp cohorts update` | Update a cohort | | `mp cohorts delete` | Delete a cohort | | `mp cohorts bulk-delete` | Delete multiple cohorts | | `mp cohorts bulk-update` | Update multiple cohorts | ### flags β€” Feature Flag Management Manage Mixpanel feature flags via the App API. Feature flags are project-scoped (no workspace ID required). | Command | Description | | ------------------------- | ---------------------------------------- | | `mp flags list` | List feature flags | | `mp flags create` | Create a new feature flag | | `mp flags get` | Get feature flag by ID | | `mp flags update` | Update a feature flag (full replacement) | | `mp flags delete` | Delete a feature flag | | `mp flags archive` | Archive a feature flag | | `mp flags restore` | Restore an archived feature flag | | `mp flags duplicate` | Duplicate a feature flag | | `mp flags set-test-users` | Set test user variant overrides | | `mp flags history` | View flag change history | | `mp flags limits` | View account-level flag usage and limits | ### experiments β€” Experiment Management Manage Mixpanel experiments via the App API. Experiments are project-scoped (no workspace ID required). | Command | Description | | -------------------------- | ------------------------------ | | `mp experiments list` | List experiments | | `mp experiments create` | Create a new experiment | | `mp experiments get` | Get experiment by ID | | `mp experiments update` | Update an experiment | | `mp experiments delete` | Delete an experiment | | `mp experiments launch` | Launch a draft experiment | | `mp experiments conclude` | Conclude an active experiment | | `mp experiments decide` | Decide experiment outcome | | `mp experiments archive` | Archive an experiment | | `mp experiments restore` | Restore an archived experiment | | `mp experiments duplicate` | Duplicate an experiment | | `mp experiments erf` | List ERF experiments | ### alerts β€” Alert Management Manage Mixpanel custom alerts via the App API. | Command | Description | | ----------------------- | ---------------------------------- | | `mp alerts list` | List custom alerts | | `mp alerts create` | Create a new alert | | `mp alerts get` | Get alert by ID | | `mp alerts update` | Update an alert | | `mp alerts delete` | Delete an alert | | `mp alerts bulk-delete` | Delete multiple alerts | | `mp alerts count` | Get alert count and limits | | `mp alerts history` | View alert trigger history | | `mp alerts test` | Send a test notification | | `mp alerts screenshot` | Get alert screenshot URL | | `mp alerts validate` | Validate alerts against a bookmark | ### annotations β€” Annotation Management Manage timeline annotations via the App API. | Command | Description | | ---------------------------- | ----------------------------------------------- | | `mp annotations list` | List annotations with optional date/tag filters | | `mp annotations create` | Create a new annotation | | `mp annotations get` | Get annotation by ID | | `mp annotations update` | Update an annotation | | `mp annotations delete` | Delete an annotation | | `mp annotations tags list` | List annotation tags | | `mp annotations tags create` | Create a new annotation tag | ### webhooks β€” Webhook Management Manage project webhooks via the App API. | Command | Description | | -------------------- | ------------------------- | | `mp webhooks list` | List project webhooks | | `mp webhooks create` | Create a new webhook | | `mp webhooks update` | Update a webhook | | `mp webhooks delete` | Delete a webhook | | `mp webhooks test` | Test webhook connectivity | ### lexicon β€” Data Governance: Lexicon Management, Enforcement, Auditing & Deletion Manage Lexicon data definitions, tags, metadata, schema enforcement, data auditing, volume anomalies, and event deletion requests via the App API. | Command | Description | | -------------------------------------- | ----------------------------------- | | `mp lexicon events get` | Get event definitions by name | | `mp lexicon events update` | Update an event definition | | `mp lexicon events delete` | Delete an event definition | | `mp lexicon events bulk-update` | Bulk-update event definitions | | `mp lexicon properties get` | Get property definitions by name | | `mp lexicon properties update` | Update a property definition | | `mp lexicon properties bulk-update` | Bulk-update property definitions | | `mp lexicon tags list` | List all Lexicon tags | | `mp lexicon tags create` | Create a new tag | | `mp lexicon tags update` | Update a tag | | `mp lexicon tags delete` | Delete a tag | | `mp lexicon tracking-metadata` | Get tracking metadata for an event | | `mp lexicon event-history` | Get change history for an event | | `mp lexicon property-history` | Get change history for a property | | `mp lexicon export` | Export Lexicon data definitions | | `mp lexicon audit` | Run schema audit to find violations | | `mp lexicon enforcement get` | Get schema enforcement settings | | `mp lexicon enforcement init` | Initialize schema enforcement | | `mp lexicon enforcement update` | Update schema enforcement (PATCH) | | `mp lexicon enforcement replace` | Replace schema enforcement (PUT) | | `mp lexicon enforcement delete` | Delete schema enforcement settings | | `mp lexicon anomalies list` | List data volume anomalies | | `mp lexicon anomalies update` | Update a data volume anomaly | | `mp lexicon anomalies bulk-update` | Bulk-update data volume anomalies | | `mp lexicon deletion-requests list` | List event deletion requests | | `mp lexicon deletion-requests create` | Create an event deletion request | | `mp lexicon deletion-requests cancel` | Cancel a pending deletion request | | `mp lexicon deletion-requests preview` | Preview deletion filter results | ### drop-filters β€” Data Governance: Drop Filter Management Manage event drop filters via the App API. | Command | Description | | ------------------------ | ---------------------------- | | `mp drop-filters list` | List all drop filters | | `mp drop-filters create` | Create a new drop filter | | `mp drop-filters update` | Update a drop filter | | `mp drop-filters delete` | Delete a drop filter | | `mp drop-filters limits` | Get drop filter usage limits | ### custom-properties β€” Data Governance: Custom Property Management Manage custom computed properties via the App API. | Command | Description | | ------------------------------- | ------------------------------------- | | `mp custom-properties list` | List all custom properties | | `mp custom-properties get` | Get a custom property by ID | | `mp custom-properties create` | Create a new custom property | | `mp custom-properties update` | Update a custom property | | `mp custom-properties delete` | Delete a custom property | | `mp custom-properties validate` | Validate a custom property definition | ### custom-events β€” Data Governance: Custom Event Management Manage custom composite events via the App API. | Command | Description | | ------------------------- | ---------------------- | | `mp custom-events list` | List all custom events | | `mp custom-events update` | Update a custom event | | `mp custom-events delete` | Delete a custom event | ### lookup-tables β€” Data Governance: Lookup Table Management Manage CSV-based lookup tables for property enrichment via the App API. | Command | Description | | ------------------------------- | ---------------------------------- | | `mp lookup-tables list` | List lookup tables | | `mp lookup-tables upload` | Upload a CSV as a new lookup table | | `mp lookup-tables update` | Update a lookup table | | `mp lookup-tables delete` | Delete lookup tables | | `mp lookup-tables download` | Download lookup table data as CSV | | `mp lookup-tables upload-url` | Get a signed upload URL | | `mp lookup-tables download-url` | Get a signed download URL | ### schemas β€” Data Governance: Schema Registry Management Manage JSON Schema Draft 7 definitions in the schema registry via the App API. | Command | Description | | ------------------------ | --------------------------------------- | | `mp schemas list` | List schema registry entries | | `mp schemas create` | Create a single schema entry | | `mp schemas create-bulk` | Bulk create schema entries | | `mp schemas update` | Update a schema entry (merge semantics) | | `mp schemas update-bulk` | Bulk update schema entries | | `mp schemas delete` | Delete schema entries | ## Output Formats All commands support the `--format` option: | Format | Description | Use Case | | ------- | -------------------- | ------------------------- | | `json` | Pretty-printed JSON | Default, human-readable | | `jsonl` | JSON Lines | Streaming, large datasets | | `table` | Rich formatted table | Terminal viewing | | `csv` | CSV with headers | Spreadsheet export | | `plain` | Minimal text | Scripting | ## Filtering with --jq Commands that output JSON also support the `--jq` option for client-side filtering using jq syntax. This enables powerful transformations without external tools. ``` # Get first 5 events mp inspect events --format json --jq '.[:5]' # Filter events by name pattern mp inspect events --format json --jq '.[] | select(startswith("User"))' # Count results mp inspect events --format json --jq 'length' # Extract specific fields from query results mp query segmentation --event Purchase --from 2025-01-01 --to 2025-01-31 \ --format json --jq '.series | to_entries | map({date: .key, count: .value})' ``` --jq requires JSON format The `--jq` option only works with `--format json` or `--format jsonl`. Using it with other formats produces an error. See the [jq manual](https://jqlang.org/manual/) for filter syntax. ### Format Examples Given this query result: **json** (default) β€” Pretty-printed, easy to read: ``` [ { "event_name": "Purchase", "count": 1523 }, { "event_name": "Signup", "count": 892 }, { "event_name": "Login", "count": 4201 } ] ``` **jsonl** β€” One object per line, ideal for streaming: ``` {"event_name": "Purchase", "count": 1523} {"event_name": "Signup", "count": 892} {"event_name": "Login", "count": 4201} ``` **table** β€” Rich ASCII table for terminal viewing: ``` ┏━━━━━━━━━━━━━┳━━━━━━━┓ ┃ EVENT NAME ┃ COUNT ┃ ┑━━━━━━━━━━━━━╇━━━━━━━┩ β”‚ Purchase β”‚ 1523 β”‚ β”‚ Signup β”‚ 892 β”‚ β”‚ Login β”‚ 4201 β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”˜ ``` **csv** β€” Headers plus comma-separated values: ``` event_name,count Purchase,1523 Signup,892 Login,4201 ``` **plain** β€” Minimal output, one value per line: ``` Purchase Signup Login ``` ### Choosing a Format ``` # Terminal viewing mp inspect events --format table # Pipe to jq for processing mp query segmentation "Purchase" --from 2025-01-01 --format json | jq '.values' # Count results mp inspect events --format plain | wc -l ``` ## Exit Codes | Code | Meaning | Exception | | ---- | -------------------- | ------------------------------------------------------------------- | | 0 | Success | β€” | | 1 | General error | `MixpanelHeadlessError`, `WorkspaceScopeError`, `AccountInUseError` | | 2 | Authentication error | `AuthenticationError`, `OAuthError` | | 3 | Invalid arguments | `ConfigError`, validation errors | | 4 | Resource not found | `AccountNotFoundError`, `ProjectNotFoundError` | | 5 | Rate limit exceeded | `RateLimitError` | | 130 | Interrupted | Ctrl+C | ## Environment Variables These resolve via `env > param > target > bridge > [active] > default_project` β€” see [Configuration β†’ Credential Resolution Chain](https://mixpanel.github.io/mixpanel-headless/getting-started/configuration/#credential-resolution-chain). | Variable | Description | | ---------------------- | -------------------------------------------------------------------------------------------------------------- | | `MP_ACCOUNT` | Active account override | | `MP_PROJECT_ID` | Project override | | `MP_WORKSPACE_ID` | Workspace override | | `MP_TARGET` | Apply a saved target (mutually exclusive with `MP_ACCOUNT`/`MP_PROJECT_ID`/`MP_WORKSPACE_ID`) | | `MP_OAUTH_TOKEN` | Static bearer token (alternative to a registered account; env-var path requires `MP_PROJECT_ID` + `MP_REGION`) | | `MP_USERNAME` | Service-account username (requires `MP_SECRET`, `MP_PROJECT_ID`, `MP_REGION`) | | `MP_SECRET` | Service-account secret (paired with `MP_USERNAME`) | | `MP_REGION` | Data residency region (`us`, `eu`, `in`) | | `MP_AUTH_FILE` | Override path to the v2 Cowork bridge file | | `MP_CONFIG_PATH` | Override config file path (`~/.mp/config.toml`) | | `MP_OAUTH_STORAGE_DIR` | Override storage root (`~/.mp`) | ## Examples ### Complete Workflow ``` # 1. Authenticate (browser PKCE; picks project, derives name from /me) mp login # 2. Explore schema mp inspect events mp inspect properties --event Purchase # 3. Run live queries mp query segmentation --event Purchase --from 2025-01-01 --to 2025-01-31 --format table ``` For explicit account names or non-browser flows, use `mp login --name personal --region us` or the two-step `mp account add` + `mp account login` (see [account β€” Account Management](#account-account-management) above). ### Piping and Scripting ``` # Built-in jq filtering (no external tools needed) mp query segmentation --event Login --from 2025-01-01 --to 2025-01-31 \ --format json --jq '.series | keys | length' # Or pipe to external jq mp query segmentation --event Login --from 2025-01-01 --to 2025-01-31 --format json \ | jq '.values."$overall"' ``` ### Streaming Data Streaming is available through the Python API: ``` import mixpanel_headless as mp ws = mp.Workspace() # Stream events for event in ws.stream_events(from_date="2025-01-01", to_date="2025-01-31"): print(event) # Stream profiles for profile in ws.stream_profiles(): print(profile) ``` ## Full Command Reference See [Commands](https://mixpanel.github.io/mixpanel-headless/cli/commands/index.md) for the complete auto-generated reference. Copy markdown # CLI Commands Complete reference for the `mp` command-line interface. Explore on DeepWiki πŸ€– **[CLI Command Reference β†’](https://deepwiki.com/mixpanel/mixpanel-headless/7.1-cli-command-reference)** Ask questions about specific commands, explore options, or get examples for your use case. ### mp Mixpanel data CLI - discover, query, and manage analytics data. Usage: ``` mp [OPTIONS] COMMAND [ARGS]... ``` Options: ``` -a, --account TEXT Account name to use (overrides default). [env var: MP_ACCOUNT] -p, --project TEXT Project ID to use (overrides active context). [env var: MP_PROJECT_ID] -w, --workspace INTEGER Workspace ID for this command. [env var: MP_WORKSPACE_ID] -t, --target TEXT Apply a saved target (mutually exclusive with --account/--project/--workspace). [env var: MP_TARGET] -q, --quiet Suppress progress output. -v, --verbose Enable debug output. --version Show version and exit. --install-completion Install completion for the current shell. --show-completion Show completion for the current shell, to copy it or customize the installation. ``` #### account Manage accounts. Usage: ``` mp account [OPTIONS] COMMAND [ARGS]... ``` ##### add Add a new account. For `service_account`, `--username` is required and the secret is read from stdin (when `--secret-stdin` is set) or from `MP_SECRET`. For `oauth_token`, supply `--token-env` (recommended) or set `MP_OAUTH_TOKEN` and we'll capture it inline. For every type, `--project` is optional β€” leave it blank and set the active project later via `mp project use ID` (or use the guided `mp login` flow, which picks one automatically). TIP: For new setups, prefer `mp login` for a guided flow. `mp account add` remains the explicit, scriptable path for CI and automation. Args: ctx: Typer context. name: Account name (alphanumeric, `_`, `-`). type: `service_account` | `oauth_browser` | `oauth_token`. region: `us` | `eu` | `in`. project: Project ID (optional; becomes the account's `default_project`). username: Required for `service_account`. secret_stdin: Read secret from stdin instead of env. token_env: Env var holding the bearer for `oauth_token`. Usage: ``` mp account add [OPTIONS] [NAME] ``` Options: ``` [NAME] Account name (alphanumeric, _, -). Optional for service_account / oauth_token (derived from the first /me organization when omitted). For oauth_browser, prefer `mp login` for the guided flow. --type TEXT One of: service_account | oauth_browser | oauth_token [required] --region TEXT Mixpanel region: us | eu | in. Optional for service_account / oauth_token (probed against /me when omitted) and for oauth_browser (defaults to us; the post- login /me cross-check catches mismatches). --project TEXT Project ID (optional; can be set later via `mp project use ID`). Falls back to MP_PROJECT_ID when omitted. For oauth_browser, also backfilled by `mp account login`. --username TEXT Username (service_account) --secret-stdin Read secret from stdin (service_account; agent-friendly). --token-env TEXT Env-var name holding the bearer (oauth_token). ``` ##### export-bridge Export the named (or active) account as a v2 bridge file at `--to`. For `oauth_browser` accounts, embeds the on-disk OAuth tokens so the consumer side (typically a Cowork VM) authenticates without re-running PKCE. Settings-side `[settings].custom_header` propagates into the bridge's `headers` block. Usage: ``` mp account export-bridge [OPTIONS] ``` Options: ``` --account TEXT Account to export (defaults to active) --to PATH Destination bridge file path. [default: mixpanel_auth.json] --project TEXT Pin a project ID into the bridge. --workspace INTEGER Pin a workspace ID into the bridge. ``` ##### list List all configured accounts. Shows the active account marker and any targets that reference each account (per FR-045, the first account auto-promotes to active). Args: ctx: Typer context. format: Output format. Usage: ``` mp account list [OPTIONS] ``` Options: ``` -f, --format TEXT Output format: table | json | jsonl. [default: table] ``` ##### login Run the OAuth browser flow for an `oauth_browser` account. Drives the PKCE login dance, persists tokens to `~/.mp/accounts/{name}/tokens.json`, and probes `/me` to backfill the account's `default_project` on first login. Prints a JSON :class:`OAuthLoginResult` so scripts and the plugin can consume the structured outcome. Args: ctx: Typer context. name: Account name (must be `oauth_browser` type). no_browser: Skip browser launch (manual URL copy). Usage: ``` mp account login [OPTIONS] NAME ``` Options: ``` NAME OAuth browser account name. [required] --no-browser Skip launching the system browser (headless / SSH). ``` ##### logout Remove the on-disk OAuth tokens for an `oauth_browser` account. Args: ctx: Typer context. name: Account name. Usage: ``` mp account logout [OPTIONS] NAME ``` Options: ``` NAME Account name to log out. [required] ``` ##### remove Remove an account. Without `--force`, raises if any target references the account. Args: ctx: Typer context. name: Account to remove. force: When `True`, remove and orphan any referencing targets. Usage: ``` mp account remove [OPTIONS] NAME ``` Options: ``` NAME Account name to remove. [required] --force Remove even if referenced by targets (orphans them). ``` ##### remove-bridge Delete the bridge file at `--at` (or the resolved default path). Idempotent β€” no error if the bridge is already absent (exit 0 either way). Usage: ``` mp account remove-bridge [OPTIONS] ``` Options: ``` --at PATH Bridge file path (defaults to standard search). ``` ##### show Show one account's summary (active by default). Args: ctx: Typer context. name: Account name; `None` shows the active account. format: Output format. Usage: ``` mp account show [OPTIONS] [NAME] ``` Options: ``` [NAME] Account name (defaults to active) -f, --format TEXT Output: table | json [default: table] ``` ##### test Probe `/me` for the named (or active) account. Never raises β€” failure is captured in the result's `error` field. Args: ctx: Typer context. name: Account to test. Usage: ``` mp account test [OPTIONS] [NAME] ``` Options: ``` [NAME] Account to test (defaults to active) ``` ##### token Print the current bearer token for an OAuth account. Returns `N/A` for service accounts (no bearer). Args: ctx: Typer context. name: Account name; `None` uses the active account. Usage: ``` mp account token [OPTIONS] [NAME] ``` Options: ``` [NAME] Account name (defaults to active) ``` ##### update Update fields on an existing account in place. Only supplied flags are changed. Type cannot be changed via this command (remove + re-add for that). Type-incompatible flags raise an error. Args: ctx: Typer context. name: Account name to update. region: New region. project: New default_project. username: New username (service_account only). secret_stdin: Read a new secret from stdin (service_account only). token_env: New env-var name (oauth_token only). Usage: ``` mp account update [OPTIONS] NAME ``` Options: ``` NAME Account name to update. [required] --region TEXT New region: us | eu | in --project TEXT New default_project (numeric project ID). --username TEXT New username (service_account only). --secret-stdin Read new secret from stdin (service_account only). --token-env TEXT New env-var name (oauth_token only). ``` ##### use Set the active account, clearing any prior workspace pin. Project travels with the account via `Account.default_project`, but workspace IDs are project-scoped β€” a workspace ID set under the prior account would resolve to a foreign workspace (or 404) under the new one, so it's dropped on every account swap. Args: ctx: Typer context. name: Account to activate. Usage: ``` mp account use [OPTIONS] NAME ``` Options: ``` NAME Account name to make active. [required] ``` #### alerts Manage Mixpanel custom alerts. Usage: ``` mp alerts [OPTIONS] COMMAND [ARGS]... ``` ##### bulk-delete Bulk-delete custom alerts by IDs. Permanently deletes multiple alerts. Provide IDs as a comma-separated string. Args: ctx: Typer context with global options. ids: Comma-separated alert IDs. Usage: ``` mp alerts bulk-delete [OPTIONS] ``` Options: ``` --ids TEXT Comma-separated alert IDs to delete. [required] ``` ##### count Get alert count and limits. Retrieves the current alert count, account limit, and whether the account is below its limit. Args: ctx: Typer context with global options. alert_type: Filter by alert type. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp alerts count [OPTIONS] ``` Options: ``` --type TEXT Filter by alert type. -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### create Create a new custom alert. Creates a custom alert linked to a saved report (bookmark). The condition, subscriptions, and notification-windows options accept JSON strings. Args: ctx: Typer context with global options. bookmark_id: Linked bookmark ID. name: Alert name. condition: Trigger condition as JSON string. frequency: Check frequency in seconds. paused: Whether to start paused. subscriptions: Notification targets as JSON string. notification_windows: Notification windows as JSON string. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp alerts create [OPTIONS] ``` Options: ``` --bookmark-id INTEGER Linked bookmark ID (required). [required] --name TEXT Alert name (required). [required] --condition TEXT Trigger condition as JSON string (required). [required] --frequency INTEGER Check frequency in seconds (required). [required] --paused / --no-paused Start paused or active. [default: no- paused] --subscriptions TEXT Notification targets as JSON string. --notification-windows TEXT Notification windows as JSON string. -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### delete Delete a custom alert. Permanently deletes a custom alert by ID. This action cannot be undone. Args: ctx: Typer context with global options. alert_id: Alert ID (integer). Usage: ``` mp alerts delete [OPTIONS] ALERT_ID ``` Options: ``` ALERT_ID Alert ID. [required] ``` ##### get Get a single custom alert by ID. Retrieves the full alert object including condition, subscriptions, and metadata. Args: ctx: Typer context with global options. alert_id: Alert ID (integer). format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp alerts get [OPTIONS] ALERT_ID ``` Options: ``` ALERT_ID Alert ID. [required] -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### history View trigger history for a custom alert. Retrieves the paginated trigger history for the specified alert. Args: ctx: Typer context with global options. alert_id: Alert ID (integer). page_size: Number of results per page. cursor: Pagination cursor for next page. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp alerts history [OPTIONS] ALERT_ID ``` Options: ``` ALERT_ID Alert ID. [required] --page-size INTEGER Results per page. --cursor TEXT Pagination cursor for next page. -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### list List custom alerts for the current project. Retrieves all custom alerts visible to the authenticated user. Optionally filter by linked bookmark or list alerts for all users. Args: ctx: Typer context with global options. bookmark_id: Filter by linked bookmark ID. skip_user_filter: Whether to list alerts for all users. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp alerts list [OPTIONS] ``` Options: ``` --bookmark-id INTEGER Filter by linked bookmark ID. --skip-user-filter List alerts for all users. -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### screenshot Get a signed URL for an alert screenshot. Retrieves a signed GCS URL that can be used to view the alert screenshot image. Args: ctx: Typer context with global options. gcs_key: GCS object key for the screenshot. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp alerts screenshot [OPTIONS] ``` Options: ``` --gcs-key TEXT GCS object key for the screenshot. [required] -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### test Send a test alert notification. Sends a test notification using the provided alert parameters without actually creating the alert. Args: ctx: Typer context with global options. bookmark_id: Linked bookmark ID. name: Alert name. condition: Trigger condition as JSON string. frequency: Check frequency in seconds. subscriptions: Notification targets as JSON string. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp alerts test [OPTIONS] ``` Options: ``` --bookmark-id INTEGER Linked bookmark ID. [required] --name TEXT Alert name. [required] --condition TEXT Trigger condition as JSON string. [required] --frequency INTEGER Check frequency in seconds. [required] --subscriptions TEXT Notification targets as JSON string. -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### update Update an existing custom alert (PATCH semantics). Only fields provided will be updated. Accepts JSON strings for condition, subscriptions, and notification-windows. Args: ctx: Typer context with global options. alert_id: Alert ID (integer). name: New alert name. bookmark_id: New linked bookmark ID. condition: New condition as JSON string. frequency: New check frequency in seconds. paused: Pause or unpause alert. subscriptions: New notification targets as JSON string. notification_windows: New notification windows as JSON string. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp alerts update [OPTIONS] ALERT_ID ``` Options: ``` ALERT_ID Alert ID. [required] --name TEXT New alert name. --bookmark-id INTEGER New linked bookmark ID. --condition TEXT New condition as JSON string. --frequency INTEGER New check frequency in seconds. --paused / --no-paused Pause or unpause alert. --subscriptions TEXT New notification targets as JSON string. --notification-windows TEXT New notification windows as JSON string. -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### validate Validate alerts against a bookmark configuration. Checks whether the specified alerts are compatible with the given bookmark type and parameters. Args: ctx: Typer context with global options. alert_ids: Comma-separated alert IDs. bookmark_type: Bookmark type to validate against. bookmark_params: Bookmark params as JSON string. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp alerts validate [OPTIONS] ``` Options: ``` --alert-ids TEXT Comma-separated alert IDs to validate. [required] --bookmark-type TEXT Bookmark type to validate against. [required] --bookmark-params TEXT Bookmark params as JSON string. [required] -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` #### annotations Manage timeline annotations. Usage: ``` mp annotations [OPTIONS] COMMAND [ARGS]... ``` ##### create Create a new timeline annotation. Creates an annotation at the specified date with the given description. Optionally associate tag IDs and a creator user ID. Args: ctx: Typer context with global options. date: Annotation date (ISO format, required). description: Annotation text (required). tags: Comma-separated tag IDs. user_id: Creator user ID. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp annotations create [OPTIONS] ``` Options: ``` --date TEXT Annotation date (ISO format, required). [required] --description TEXT Annotation text (required). [required] --tags TEXT Comma-separated tag IDs to associate. --user-id INTEGER Creator user ID. -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### delete Delete an annotation. Permanently deletes an annotation by ID. This action cannot be undone. Args: ctx: Typer context with global options. annotation_id: Annotation identifier. Usage: ``` mp annotations delete [OPTIONS] ANNOTATION_ID ``` Options: ``` ANNOTATION_ID Annotation ID. [required] ``` ##### get Get a single annotation by ID. Retrieves the full annotation object including tags and user info. Args: ctx: Typer context with global options. annotation_id: Annotation identifier. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp annotations get [OPTIONS] ANNOTATION_ID ``` Options: ``` ANNOTATION_ID Annotation ID. [required] -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### list List timeline annotations. Retrieves annotations for the project, optionally filtered by date range or tag IDs. Args: ctx: Typer context with global options. from_date: Start date filter (ISO format). to_date: End date filter (ISO format). tags: Comma-separated tag IDs. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp annotations list [OPTIONS] ``` Options: ``` --from TEXT Start date filter (ISO format). --to TEXT End date filter (ISO format). --tags TEXT Comma-separated tag IDs to filter by. -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### tags Manage annotation tags. Usage: ``` mp annotations tags [OPTIONS] COMMAND [ARGS]... ``` ###### create Create a new annotation tag. Creates a tag that can be associated with annotations. Args: ctx: Typer context with global options. name: Tag name (required). format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp annotations tags create [OPTIONS] ``` Options: ``` --name TEXT Tag name (required). [required] -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ###### list List annotation tags. Retrieves all annotation tags for the project. Args: ctx: Typer context with global options. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp annotations tags list [OPTIONS] ``` Options: ``` -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### update Update an existing annotation. Updates the specified annotation using PATCH semantics. Only provided fields are changed. Args: ctx: Typer context with global options. annotation_id: Annotation identifier. description: New description. tags: Comma-separated tag IDs (replaces existing). format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp annotations update [OPTIONS] ANNOTATION_ID ``` Options: ``` ANNOTATION_ID Annotation ID. [required] --description TEXT New description. --tags TEXT Comma-separated tag IDs (replaces existing). -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` #### business-context Read and write project / organization business context. Usage: ``` mp business-context [OPTIONS] COMMAND [ARGS]... ``` ##### chain Read both org-level and project-level context in one call. Calls the project-scoped `/business-context/chain` endpoint and returns a `BusinessContextChain` JSON document with both scopes populated. `organization.organization_id` is populated only when a cached `/me` response is available β€” this preserves the chain endpoint's single-network-round-trip guarantee. Args: ctx: Typer context with global options. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp business-context chain [OPTIONS] ``` Options: ``` -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### clear Clear business context at the given scope. Equivalent to `mp business-context set --content ""`. Documents intent and avoids accidental clearing when stdin is unexpectedly empty. Args: ctx: Typer context with global options. level: `project` (default) or `organization`. organization_id: Optional explicit org ID for org-level clears. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp business-context clear [OPTIONS] ``` Options: ``` --level [organization|project] Which scope to operate on. [default: project] --organization-id INTEGER Organization ID for --level organization. When omitted, auto-resolved from the current session's project via the cached /me response. -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### get Read business context content at the given scope. Outputs a `BusinessContext` JSON document with `level`, `content`, and the matching `organization_id` or `project_id`. Args: ctx: Typer context with global options. level: `project` (default) or `organization`. organization_id: Optional explicit org ID for org-level reads. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp business-context get [OPTIONS] ``` Options: ``` --level [organization|project] Which scope to operate on. [default: project] --organization-id INTEGER Organization ID for --level organization. When omitted, auto-resolved from the current session's project via the cached /me response. -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### set Replace business context content at the given scope. Content can come from `--content`, `--file`, or piped stdin (when neither flag is given and stdin is not a TTY). Empty / whitespace-only stdin is rejected β€” use `mp business-context clear` or pass `--content ""` explicitly to clear, to prevent silent destructive writes in CI / cron. The 50,000-character limit is enforced client-side BEFORE the HTTP call so over-long input fails immediately rather than wasting a round-trip. Args: ctx: Typer context with global options. level: `project` (default) or `organization`. organization_id: Optional explicit org ID for org-level writes. content: Inline content. Mutually exclusive with `--file` / stdin. file: Path to a file containing the content. Mutually exclusive with `--content` / stdin. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp business-context set [OPTIONS] ``` Options: ``` --level [organization|project] Which scope to operate on. [default: project] --organization-id INTEGER Organization ID for --level organization. When omitted, auto-resolved from the current session's project via the cached /me response. --content TEXT Inline markdown content. Mutually exclusive with --file / stdin. --file PATH Read markdown content from a file. Mutually exclusive with --content / stdin. -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` #### cohorts Manage Mixpanel cohorts. Usage: ``` mp cohorts [OPTIONS] COMMAND [ARGS]... ``` ##### bulk-delete Delete multiple cohorts at once. Permanently removes all cohorts whose IDs are provided. Args: ctx: Typer context with global options. ids: Comma-separated cohort IDs to delete. Usage: ``` mp cohorts bulk-delete [OPTIONS] ``` Options: ``` --ids TEXT Comma-separated list of cohort IDs to delete (required). [required] ``` ##### bulk-update Update multiple cohorts at once. Accepts a JSON array of update entries. Each entry must include an `id` field and may include `name`, `description`, and `definition` fields. Args: ctx: Typer context with global options. entries: JSON string with a list of update entries. Example usage:: ``` mp cohorts bulk-update --entries '[{"id": 1, "name": "Renamed"}]' ``` Usage: ``` mp cohorts bulk-update [OPTIONS] ``` Options: ``` --entries TEXT JSON string containing a list of cohort update entries. Each entry must have an "id" field and optional "name", "description", and "definition" fields. [required] ``` ##### create Create a new cohort. Creates a cohort with the given name and optional description, data group, and behavioral definition. Args: ctx: Typer context with global options. name: Cohort name. description: Optional cohort description. data_group_id: Optional data group identifier. definition: Optional cohort definition as a JSON string. format: Output format (json, jsonl, table, csv, plain). jq_filter: Optional jq filter for JSON output. Usage: ``` mp cohorts create [OPTIONS] ``` Options: ``` --name TEXT Cohort name (required). [required] --description TEXT Cohort description. --data-group-id TEXT Data group identifier. --definition TEXT Cohort definition as a JSON string. -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### delete Delete a single cohort. Permanently removes the cohort with the given ID. Args: ctx: Typer context with global options. cohort_id: The cohort identifier. Usage: ``` mp cohorts delete [OPTIONS] COHORT_ID ``` Options: ``` COHORT_ID Cohort ID to delete. [required] ``` ##### get Get a single cohort by ID. Retrieves full cohort details including metadata, definition, and creator information. Args: ctx: Typer context with global options. cohort_id: The cohort identifier. format: Output format (json, jsonl, table, csv, plain). jq_filter: Optional jq filter for JSON output. Usage: ``` mp cohorts get [OPTIONS] COHORT_ID ``` Options: ``` COHORT_ID Cohort ID to retrieve. [required] -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### list List cohorts from the Mixpanel App API. Returns full cohort objects with all metadata. Optionally filter by data group ID or a specific set of cohort IDs. Args: ctx: Typer context with global options. data_group_id: Optional data group filter. ids: Comma-separated cohort IDs to retrieve. format: Output format (json, jsonl, table, csv, plain). jq_filter: Optional jq filter for JSON output. Usage: ``` mp cohorts list [OPTIONS] ``` Options: ``` --data-group-id TEXT Filter cohorts by data group ID. --ids TEXT Comma-separated list of cohort IDs to filter by. -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### update Update an existing cohort. Updates the specified cohort with any provided fields. Only fields that are explicitly set will be sent to the API. Args: ctx: Typer context with global options. cohort_id: The cohort identifier. name: Optional new cohort name. description: Optional new cohort description. definition: Optional new cohort definition as a JSON string. format: Output format (json, jsonl, table, csv, plain). jq_filter: Optional jq filter for JSON output. Usage: ``` mp cohorts update [OPTIONS] COHORT_ID ``` Options: ``` COHORT_ID Cohort ID to update. [required] --name TEXT New cohort name. --description TEXT New cohort description. --definition TEXT New cohort definition as a JSON string. -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` #### custom-events Manage custom events. Usage: ``` mp custom-events [OPTIONS] COMMAND [ARGS]... ``` ##### create Create a new custom event. A custom event aliases one or more underlying events under a single display name. For example:: ``` mp custom-events create --name "Page View" \ --alternative "Home Viewed" --alternative "Product Viewed" ``` Args: ctx: Typer context with global options. name: Display name for the new custom event. alternative: Underlying event names to alias. Repeatable. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp custom-events create [OPTIONS] ``` Options: ``` --name TEXT Display name for the new custom event. [required] --alternative TEXT Underlying event name to alias. Pass --alternative multiple times to alias more than one event. [required] -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### delete Delete a custom event. Identify the custom event by either `--id` (preferred) or `--name`. Name lookup uses :meth:`Workspace.list_custom_events` and errors if the name is ambiguous or unknown. The DELETE always targets the resolved `custom_event_id` so the data-definitions endpoint cannot silently delete the wrong entry or a stray orphan lexicon row. Permanently deletes a custom event. This action cannot be undone. Args: ctx: Typer context with global options. custom_event_id: Custom event ID. Mutually exclusive with name. name: Custom event name. Resolved to an ID via list_custom_events. Usage: ``` mp custom-events delete [OPTIONS] ``` Options: ``` --id, --custom-event-id INTEGER Custom event ID. Use this OR --name. Prefer --id when known. --name TEXT Custom event name. Resolved to an ID via list_custom_events; errors if zero or multiple custom events share the name. ``` ##### list List all custom events for the current project. Retrieves all custom event definitions in the project. Args: ctx: Typer context with global options. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp custom-events list [OPTIONS] ``` Options: ``` -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### update Update a custom event's lexicon entry. Identify the custom event by either `--id` (preferred) or `--name`. Name lookup uses :meth:`Workspace.list_custom_events` and errors if the name is ambiguous or unknown. Args: ctx: Typer context with global options. custom_event_id: Custom event ID. Mutually exclusive with name. name: Custom event name. Resolved to an ID via list_custom_events. hidden: Hide or show the event. dropped: Drop or undrop the event. description: New event description. verified: Mark as verified or not. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp custom-events update [OPTIONS] ``` Options: ``` --id, --custom-event-id INTEGER Custom event ID. Use this OR --name. Prefer --id when known. --name TEXT Custom event name. Resolved to an ID via list_custom_events; errors if zero or multiple custom events share the name. --hidden / --no-hidden Hide or show the event. --dropped / --no-dropped Drop or undrop the event. --description TEXT New event description. --verified / --no-verified Mark as verified or not. -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` #### custom-properties Manage custom properties. Usage: ``` mp custom-properties [OPTIONS] COMMAND [ARGS]... ``` ##### create Create a new custom property. Creates a custom property with either a display formula or behavior specification. The --display-formula and --behavior options are mutually exclusive. When using --display-formula, --composed-properties is required. Args: ctx: Typer context with global options. name: Property name. resource_type: Resource type (events, people, group_profiles). display_formula: Formula expression. composed_properties: Referenced properties as JSON string. behavior: Behavior specification as JSON string. description: Property description. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp custom-properties create [OPTIONS] ``` Options: ``` --name TEXT Property name (required). [required] --resource-type TEXT Resource type: events, people, group_profiles (required). [required] --display-formula TEXT Formula expression (mutually exclusive with --behavior). --composed-properties TEXT Referenced properties as JSON string. --behavior TEXT Behavior specification as JSON string (mutually exclusive with --display-formula). --description TEXT Property description. -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### delete Delete a custom property. Permanently deletes a custom property by ID. This action cannot be undone. Args: ctx: Typer context with global options. id: Custom property ID (string). Usage: ``` mp custom-properties delete [OPTIONS] ``` Options: ``` --id TEXT Custom property ID (required). [required] ``` ##### get Get a single custom property by ID. Retrieves the full custom property object including its formula, composed properties, and metadata. Args: ctx: Typer context with global options. id: Custom property ID (string). format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp custom-properties get [OPTIONS] ``` Options: ``` --id TEXT Custom property ID (required). [required] -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### list List all custom properties for the current project. Retrieves all custom properties defined in the project. Args: ctx: Typer context with global options. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp custom-properties list [OPTIONS] ``` Options: ``` -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### update Update an existing custom property (PUT semantics). Updates a custom property by ID. Note that resource_type and data_group_id are immutable and cannot be changed. Args: ctx: Typer context with global options. id: Custom property ID. name: New property name. description: New property description. display_formula: New formula expression. composed_properties: New referenced properties as JSON string. is_locked: Lock or unlock the property. is_visible: Show or hide the property. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp custom-properties update [OPTIONS] ``` Options: ``` --id TEXT Custom property ID (required). [required] --name TEXT New property name. --description TEXT New property description. --display-formula TEXT New formula expression. --composed-properties TEXT New referenced properties as JSON string. --is-locked / --no-is-locked Lock or unlock the property. --is-visible / --no-is-visible Show or hide the property. -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### validate Validate a custom property formula without creating it. Checks whether the provided custom property definition is valid. Does not create the property. Args: ctx: Typer context with global options. name: Property name. resource_type: Resource type (events, people, group_profiles). display_formula: Formula expression to validate. composed_properties: Referenced properties as JSON string. behavior: Behavior specification as JSON string. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp custom-properties validate [OPTIONS] ``` Options: ``` --name TEXT Property name (required). [required] --resource-type TEXT Resource type: events, people, group_profiles (required). [required] --display-formula TEXT Formula expression to validate. --composed-properties TEXT Referenced properties as JSON string. --behavior TEXT Behavior specification as JSON string. -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` #### dashboards Manage Mixpanel dashboards. Usage: ``` mp dashboards [OPTIONS] COMMAND [ARGS]... ``` ##### add-report Add a report to a dashboard. Clones the specified bookmark/report onto the dashboard and returns the updated dashboard. Args: ctx: Typer context with global options. dashboard_id: Dashboard identifier. bookmark_id: Bookmark/report identifier to add. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp dashboards add-report [OPTIONS] DASHBOARD_ID BOOKMARK_ID ``` Options: ``` DASHBOARD_ID Dashboard ID. [required] BOOKMARK_ID Bookmark/report ID to add. [required] -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### blueprint-create Create a dashboard from a blueprint template. Creates a new dashboard using the specified blueprint template type. Args: ctx: Typer context with global options. template_type: Blueprint template type identifier. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp dashboards blueprint-create [OPTIONS] TEMPLATE_TYPE ``` Options: ``` TEMPLATE_TYPE Blueprint template type identifier. [required] -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### blueprints List available dashboard blueprint templates. Retrieves the catalog of blueprint templates that can be used to create pre-configured dashboards. Args: ctx: Typer context with global options. include_reports: Whether to include report details. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp dashboards blueprints [OPTIONS] ``` Options: ``` --include-reports Include report details in templates. -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### bulk-delete Delete multiple dashboards. Permanently deletes all specified dashboards. This action cannot be undone. Args: ctx: Typer context with global options. ids: Comma-separated dashboard IDs to delete (required). Usage: ``` mp dashboards bulk-delete [OPTIONS] ``` Options: ``` --ids TEXT Comma-separated dashboard IDs to delete. [required] ``` ##### create Create a new dashboard. Creates a dashboard with the specified title and optional settings. Use --duplicate to clone an existing dashboard. Args: ctx: Typer context with global options. title: Dashboard title (required). description: Optional dashboard description. private: Whether to make the dashboard private. restricted: Whether to restrict dashboard access. duplicate: ID of an existing dashboard to duplicate. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp dashboards create [OPTIONS] ``` Options: ``` --title TEXT Dashboard title. [required] --description TEXT Dashboard description. --private / --no-private Set privacy (default: not set). --restricted / --no-restricted Set restriction (default: not set). --duplicate INTEGER ID of dashboard to duplicate. -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### delete Delete a dashboard. Permanently deletes a dashboard by ID. This action cannot be undone. Args: ctx: Typer context with global options. dashboard_id: Dashboard identifier. Usage: ``` mp dashboards delete [OPTIONS] DASHBOARD_ID ``` Options: ``` DASHBOARD_ID Dashboard ID. [required] ``` ##### erf Get ERF data for a dashboard. Retrieves the ERF metrics data associated with the specified dashboard. Args: ctx: Typer context with global options. dashboard_id: Dashboard identifier. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp dashboards erf [OPTIONS] DASHBOARD_ID ``` Options: ``` DASHBOARD_ID Dashboard ID. [required] -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### favorite Favorite a dashboard. Marks the specified dashboard as a favorite for the current user. Args: ctx: Typer context with global options. dashboard_id: Dashboard identifier. Usage: ``` mp dashboards favorite [OPTIONS] DASHBOARD_ID ``` Options: ``` DASHBOARD_ID Dashboard ID. [required] ``` ##### get Get a single dashboard by ID. Retrieves the full dashboard object including metadata, permissions, and layout. Args: ctx: Typer context with global options. dashboard_id: Dashboard identifier. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp dashboards get [OPTIONS] DASHBOARD_ID ``` Options: ``` DASHBOARD_ID Dashboard ID. [required] -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### list List dashboards for the current project. Retrieves all dashboards visible to the authenticated user, optionally filtered by specific IDs. Args: ctx: Typer context with global options. ids: Optional comma-separated dashboard IDs to filter by. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp dashboards list [OPTIONS] ``` Options: ``` --ids TEXT Comma-separated dashboard IDs to filter by. -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### pin Pin a dashboard. Pins the specified dashboard for the current project. Args: ctx: Typer context with global options. dashboard_id: Dashboard identifier. Usage: ``` mp dashboards pin [OPTIONS] DASHBOARD_ID ``` Options: ``` DASHBOARD_ID Dashboard ID. [required] ``` ##### rca Create an RCA (Root Cause Analysis) dashboard. Creates a dashboard for root cause analysis using the specified source ID and source data configuration. Args: ctx: Typer context with global options. source_id: Source ID for RCA analysis (required). source_data: RCA source data as a JSON string (required). format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp dashboards rca [OPTIONS] ``` Options: ``` --source-id INTEGER Source ID for RCA analysis. [required] --source-data TEXT RCA source data as JSON string (e.g. '{"type": "anomaly"}'). [required] -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### remove-report Remove a report from a dashboard. Removes the specified bookmark/report from the dashboard and returns the updated dashboard. Args: ctx: Typer context with global options. dashboard_id: Dashboard identifier. bookmark_id: Bookmark/report identifier to remove. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp dashboards remove-report [OPTIONS] DASHBOARD_ID BOOKMARK_ID ``` Options: ``` DASHBOARD_ID Dashboard ID. [required] BOOKMARK_ID Bookmark/report ID to remove. [required] -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### unfavorite Unfavorite a dashboard. Removes the specified dashboard from the current user's favorites. Args: ctx: Typer context with global options. dashboard_id: Dashboard identifier. Usage: ``` mp dashboards unfavorite [OPTIONS] DASHBOARD_ID ``` Options: ``` DASHBOARD_ID Dashboard ID. [required] ``` ##### unpin Unpin a dashboard. Unpins the specified dashboard from the current project. Args: ctx: Typer context with global options. dashboard_id: Dashboard identifier. Usage: ``` mp dashboards unpin [OPTIONS] DASHBOARD_ID ``` Options: ``` DASHBOARD_ID Dashboard ID. [required] ``` ##### update Update an existing dashboard. Updates the specified fields on a dashboard. Only provided options are sent to the API; omitted fields are unchanged. Args: ctx: Typer context with global options. dashboard_id: Dashboard identifier. title: New title for the dashboard. description: New description for the dashboard. private: Set privacy (use --no-private to unset). restricted: Set restriction (use --no-restricted to unset). format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp dashboards update [OPTIONS] DASHBOARD_ID ``` Options: ``` DASHBOARD_ID Dashboard ID. [required] --title TEXT New dashboard title. --description TEXT New dashboard description. --private / --no-private Set privacy. --restricted / --no-restricted Set restriction. -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### update-report-link Update a report link on a dashboard. Changes the type of a report link on the specified dashboard. Args: ctx: Typer context with global options. dashboard_id: Dashboard identifier. report_link_id: Report link identifier. link_type: Link type value (required). Usage: ``` mp dashboards update-report-link [OPTIONS] DASHBOARD_ID REPORT_LINK_ID ``` Options: ``` DASHBOARD_ID Dashboard ID. [required] REPORT_LINK_ID Report link ID. [required] --type TEXT Link type (e.g. 'embedded'). [required] ``` ##### update-text-card Update a text card on a dashboard. Updates the markdown content of a text card on the specified dashboard. Args: ctx: Typer context with global options. dashboard_id: Dashboard identifier. text_card_id: Text card identifier. markdown: Markdown content for the text card. Usage: ``` mp dashboards update-text-card [OPTIONS] DASHBOARD_ID TEXT_CARD_ID ``` Options: ``` DASHBOARD_ID Dashboard ID. [required] TEXT_CARD_ID Text card ID. [required] --markdown TEXT Markdown content for the text card. ``` #### drop-filters Manage drop filters. Usage: ``` mp drop-filters [OPTIONS] COMMAND [ARGS]... ``` ##### create Create a new drop filter. Creates a drop filter for the specified event with the given filter conditions. Returns the full list of drop filters after creation. Args: ctx: Typer context with global options. event_name: Event name to filter. filters: Filter condition as JSON string. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp drop-filters create [OPTIONS] ``` Options: ``` --event-name TEXT Event name to filter (required). [required] --filters TEXT Filter condition as JSON string (required). [required] -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### delete Delete a drop filter. Permanently deletes a drop filter by ID. Returns the remaining list of drop filters. Args: ctx: Typer context with global options. id: Drop filter ID. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp drop-filters delete [OPTIONS] ``` Options: ``` --id INTEGER Drop filter ID (required). [required] -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### limits Get drop filter usage limits. Retrieves the current count and maximum allowed drop filters for the project. Args: ctx: Typer context with global options. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp drop-filters limits [OPTIONS] ``` Options: ``` -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### list List all drop filters for the current project. Retrieves all drop filters configured for the project. Args: ctx: Typer context with global options. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp drop-filters list [OPTIONS] ``` Options: ``` -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### update Update an existing drop filter. Updates a drop filter by ID. Only provided fields are changed. Returns the full list of drop filters after update. Args: ctx: Typer context with global options. id: Drop filter ID. event_name: New event name. filters: New filter condition as JSON string. active: Enable or disable the filter. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp drop-filters update [OPTIONS] ``` Options: ``` --id INTEGER Drop filter ID (required). [required] --event-name TEXT New event name. --filters TEXT New filter condition as JSON string. --active / --no-active Enable or disable the filter. -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` #### experiments Manage Mixpanel experiments. Usage: ``` mp experiments [OPTIONS] COMMAND [ARGS]... ``` ##### archive Archive an experiment. Marks the specified experiment as archived. Archived experiments are hidden from default listings. Args: ctx: Typer context with global options. experiment_id: Experiment identifier. Usage: ``` mp experiments archive [OPTIONS] EXPERIMENT_ID ``` Options: ``` EXPERIMENT_ID Experiment ID. [required] ``` ##### conclude Conclude an active experiment. Transitions an experiment from active to concluded status, optionally specifying an end date. Args: ctx: Typer context with global options. experiment_id: Experiment identifier. end_date: Optional end date in YYYY-MM-DD format. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp experiments conclude [OPTIONS] EXPERIMENT_ID ``` Options: ``` EXPERIMENT_ID Experiment ID. [required] --end-date TEXT End date for the experiment (YYYY-MM-DD). -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### create Create a new experiment. Creates an experiment with the specified name and optional settings. Args: ctx: Typer context with global options. name: Experiment name (required). description: Optional experiment description. hypothesis: Optional experiment hypothesis. settings: Optional experiment settings as a JSON string. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp experiments create [OPTIONS] ``` Options: ``` --name TEXT Experiment name. [required] --description TEXT Experiment description. --hypothesis TEXT Experiment hypothesis. --settings TEXT Experiment settings as JSON string (e.g. '{"confidence_level": 0.95}'). -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### decide Decide the outcome of a concluded experiment. Records the decision for a concluded experiment, including whether it was successful and which variant won. Args: ctx: Typer context with global options. experiment_id: Experiment identifier. success: Whether the experiment was successful. variant: Optional winning variant name. message: Optional decision message or rationale. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp experiments decide [OPTIONS] EXPERIMENT_ID ``` Options: ``` EXPERIMENT_ID Experiment ID. [required] --success / --no-success Whether the experiment was successful. [required] --variant TEXT Winning variant name. --message TEXT Decision message or rationale. -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### delete Delete an experiment. Permanently deletes an experiment by ID. This action cannot be undone. Args: ctx: Typer context with global options. experiment_id: Experiment identifier. Usage: ``` mp experiments delete [OPTIONS] EXPERIMENT_ID ``` Options: ``` EXPERIMENT_ID Experiment ID. [required] ``` ##### duplicate Duplicate an experiment. Creates a copy of the specified experiment with a new name. A name is required because the Mixpanel API does not support duplication without one. Args: ctx: Typer context with global options. experiment_id: Experiment identifier. name: Name for the duplicated experiment (required). format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp experiments duplicate [OPTIONS] EXPERIMENT_ID ``` Options: ``` EXPERIMENT_ID Experiment ID. [required] --name TEXT Name for the duplicated experiment (required). [required] -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### erf List ERF experiments. Retrieves ERF experiment data for the current project. Args: ctx: Typer context with global options. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp experiments erf [OPTIONS] ``` Options: ``` -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### get Get a single experiment by ID. Retrieves the full experiment object including metadata, variants, metrics, and status. Args: ctx: Typer context with global options. experiment_id: Experiment identifier. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp experiments get [OPTIONS] EXPERIMENT_ID ``` Options: ``` EXPERIMENT_ID Experiment ID. [required] -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### launch Launch a draft experiment. Transitions an experiment from draft to active status. Args: ctx: Typer context with global options. experiment_id: Experiment identifier. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp experiments launch [OPTIONS] EXPERIMENT_ID ``` Options: ``` EXPERIMENT_ID Experiment ID. [required] -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### list List experiments for the current project. Retrieves all experiments visible to the authenticated user, optionally including archived experiments. Args: ctx: Typer context with global options. include_archived: Whether to include archived experiments. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp experiments list [OPTIONS] ``` Options: ``` --include-archived Include archived experiments in results. -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### restore Restore an archived experiment. Restores a previously archived experiment back to its prior state. Args: ctx: Typer context with global options. experiment_id: Experiment identifier. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp experiments restore [OPTIONS] EXPERIMENT_ID ``` Options: ``` EXPERIMENT_ID Experiment ID. [required] -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### update Update an existing experiment. Updates the specified fields on an experiment. Only provided options are sent to the API; omitted fields are unchanged. Args: ctx: Typer context with global options. experiment_id: Experiment identifier. name: New name for the experiment. description: New description for the experiment. hypothesis: New hypothesis for the experiment. variants: Variants as a JSON string. metrics: Metrics as a JSON string. settings: Settings as a JSON string. tags: Comma-separated tags. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp experiments update [OPTIONS] EXPERIMENT_ID ``` Options: ``` EXPERIMENT_ID Experiment ID. [required] --name TEXT New experiment name. --description TEXT New experiment description. --hypothesis TEXT New experiment hypothesis. --variants TEXT Variants as JSON object string (e.g. '{"control": {"weight": 50}}'). --metrics TEXT Metrics as JSON object string (e.g. '{"primary": "Purchase"}'). --settings TEXT Settings as JSON string (e.g. '{"confidence_level": 0.95}'). --tags TEXT Comma-separated tags. -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` #### flags Manage Mixpanel feature flags. Usage: ``` mp flags [OPTIONS] COMMAND [ARGS]... ``` ##### archive Archive a feature flag. Soft-deletes a feature flag by moving it to the archived state. Archived flags are excluded from default listings. Args: ctx: Typer context with global options. flag_id: Feature flag identifier. Usage: ``` mp flags archive [OPTIONS] FLAG_ID ``` Options: ``` FLAG_ID Feature flag ID. [required] ``` ##### create Create a new feature flag. Creates a feature flag with the specified name and key. Optional parameters allow setting description, status, tags, serving method, and initial ruleset. Args: ctx: Typer context with global options. name: Flag name (required). key: Unique machine-readable key (required). description: Optional flag description. status: Initial status (enabled, disabled, archived). tags: Comma-separated tags. serving_method: Serving method (client, server, remote_or_local, remote_only). ruleset: Ruleset as a JSON string. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp flags create [OPTIONS] ``` Options: ``` --name TEXT Flag name. [required] --key TEXT Unique machine-readable key. [required] --description TEXT Flag description. --status TEXT Initial status (enabled, disabled, archived). --tags TEXT Comma-separated tags. --serving-method TEXT Serving method (client, server, remote_or_local, remote_only). --ruleset TEXT Ruleset as JSON string. -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### delete Delete a feature flag. Permanently deletes a feature flag by ID. This action cannot be undone. Args: ctx: Typer context with global options. flag_id: Feature flag identifier. Usage: ``` mp flags delete [OPTIONS] FLAG_ID ``` Options: ``` FLAG_ID Feature flag ID. [required] ``` ##### duplicate Duplicate an existing feature flag. Creates a copy of the specified feature flag with a new ID. Args: ctx: Typer context with global options. flag_id: Feature flag identifier. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp flags duplicate [OPTIONS] FLAG_ID ``` Options: ``` FLAG_ID Feature flag ID. [required] -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### get Get a single feature flag by ID. Retrieves the full feature flag object including configuration, metadata, and permissions. Args: ctx: Typer context with global options. flag_id: Feature flag identifier. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp flags get [OPTIONS] FLAG_ID ``` Options: ``` FLAG_ID Feature flag ID. [required] -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### history View change history for a feature flag. Retrieves the paginated change history for the specified feature flag, including status changes, rule updates, etc. Args: ctx: Typer context with global options. flag_id: Feature flag identifier. page: Pagination cursor for next page. page_size: Number of results per page. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp flags history [OPTIONS] FLAG_ID ``` Options: ``` FLAG_ID Feature flag ID. [required] --page TEXT Pagination cursor. --page-size INTEGER Results per page. -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### limits View account-level feature flag usage and limits. Retrieves the current flag usage, maximum limit, trial status, and contract status for the account. Args: ctx: Typer context with global options. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp flags limits [OPTIONS] ``` Options: ``` -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### list List feature flags for the current project. Retrieves all feature flags visible to the authenticated user. By default, archived flags are excluded. Args: ctx: Typer context with global options. include_archived: Whether to include archived flags. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp flags list [OPTIONS] ``` Options: ``` --include-archived Include archived flags in the listing. -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### restore Restore an archived feature flag. Restores a previously archived feature flag, returning it to its prior state. Args: ctx: Typer context with global options. flag_id: Feature flag identifier. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp flags restore [OPTIONS] FLAG_ID ``` Options: ``` FLAG_ID Feature flag ID. [required] -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### set-test-users Set test user variant overrides on a feature flag. Assigns specific users to specific variants for testing purposes. The users parameter is a JSON mapping of variant keys to user distinct IDs. Args: ctx: Typer context with global options. flag_id: Feature flag identifier. users: Test users as a JSON string mapping variant keys to user IDs. Usage: ``` mp flags set-test-users [OPTIONS] FLAG_ID ``` Options: ``` FLAG_ID Feature flag ID. [required] --users TEXT Test users as JSON string (e.g. '{"on": "user-1", "off": "user-2"}'). [required] ``` ##### update Update an existing feature flag (full replacement). Performs a full replacement of the feature flag. All required fields (name, key, status, ruleset) must be provided. Args: ctx: Typer context with global options. flag_id: Feature flag identifier. name: Flag name (required). key: Unique machine-readable key (required). status: Target status (required). ruleset: Complete ruleset as JSON string (required). description: Optional flag description. tags: Comma-separated tags. context: Optional flag context identifier. serving_method: Serving method. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp flags update [OPTIONS] FLAG_ID ``` Options: ``` FLAG_ID Feature flag ID. [required] --name TEXT Flag name (required for full replacement). [required] --key TEXT Unique machine-readable key (required). [required] --status TEXT Target status (enabled, disabled, archived) (required). [required] --ruleset TEXT Complete ruleset as JSON string (required). [required] --description TEXT Flag description. --tags TEXT Comma-separated tags. --context TEXT Flag context identifier (e.g. 'distinct_id'). --serving-method TEXT Serving method (client, server, remote_or_local, remote_only). -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` #### inspect Inspect Mixpanel project schema. Usage: ``` mp inspect [OPTIONS] COMMAND [ARGS]... ``` ##### bookmarks List saved reports (bookmarks) in Mixpanel project. Calls the Mixpanel API to retrieve saved report definitions. Use the bookmark ID with 'mp query saved-report' or 'mp query flows'. Output Structure (JSON): ``` [ {"id": 98765, "name": "Weekly KPIs", "type": "insights", "modified": "2024-01-15T10:30:00"}, {"id": 98766, "name": "Conversion Funnel", "type": "funnels", "modified": "2024-01-14T15:45:00"}, {"id": 98767, "name": "User Retention", "type": "retention", "modified": "2024-01-13T09:20:00"} ] ``` Examples: ``` mp inspect bookmarks mp inspect bookmarks --type insights mp inspect bookmarks --type funnels --format table ``` **jq Examples:** ``` --jq '[.[] | select(.type == "insights")]' # Get bookmarks by type --jq '[.[].id]' # Get bookmark IDs only --jq 'sort_by(.modified) | reverse' # Sort by modified date (newest first) --jq '.[] | select(.name | test("KPI"; "i"))' # Find bookmark by name ``` Usage: ``` mp inspect bookmarks [OPTIONS] ``` Options: ``` -t, --type TEXT Filter by type: insights, funnels, retention, flows, launch-analysis. -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### cohorts List saved cohorts in Mixpanel project. Calls the Mixpanel API to retrieve saved cohort definitions. Shows cohort ID, name, user count, and description. Output Structure (JSON): ``` [ {"id": 1001, "name": "Power Users", "count": 5420, "description": "Users with 10+ sessions"}, {"id": 1002, "name": "Trial Users", "count": 892, "description": "Active trial accounts"}, {"id": 1003, "name": "Churned", "count": 2341, "description": "No activity in 30 days"} ] ``` Examples: ``` mp inspect cohorts mp inspect cohorts --format table ``` **jq Examples:** ``` --jq '[.[] | select(.count > 1000)]' # Cohorts with more than 1000 users --jq '[.[].name]' # Get cohort names only --jq 'sort_by(.count) | reverse' # Sort by user count descending --jq '.[] | select(.name == "Power Users")' # Find cohort by name ``` Usage: ``` mp inspect cohorts [OPTIONS] ``` Options: ``` -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### coverage Show property coverage statistics from Mixpanel. Uses JQL to count how often each property is defined (non-null) vs undefined. Useful for data quality assessment. Output Structure (JSON): ``` { "event": "Purchase", "from_date": "2024-01-01", "to_date": "2024-01-31", "total_events": 5000, "coverage": [ {"property": "amount", "defined_count": 5000, "null_count": 0, "coverage_percentage": 100.0}, {"property": "coupon_code", "defined_count": 1250, "null_count": 3750, "coverage_percentage": 25.0}, {"property": "referrer", "defined_count": 4500, "null_count": 500, "coverage_percentage": 90.0} ] } ``` Examples: ``` mp inspect coverage -e Purchase -p coupon_code,referrer --from 2024-01-01 --to 2024-01-31 ``` **jq Examples:** ``` --jq '.coverage | [.[] | select(.coverage_percentage < 50)]' # Properties with low coverage --jq '.coverage | [.[] | select(.coverage_percentage == 100)]' # Fully covered properties --jq '.coverage | [.[].property]' # Get property names only --jq '.coverage | sort_by(.coverage_percentage)' # Sort by coverage percentage ``` Usage: ``` mp inspect coverage [OPTIONS] ``` Options: ``` -e, --event TEXT Event name to analyze. [required] -p, --properties TEXT Comma-separated property names to check. [required] --from TEXT Start date (YYYY-MM-DD). [required] --to TEXT End date (YYYY-MM-DD). [required] -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### daily Show daily event counts from Mixpanel. Uses JQL to count events by day. Optionally filter to specific events. Useful for understanding activity trends over time. Output Structure (JSON): ``` { "from_date": "2024-01-01", "to_date": "2024-01-07", "events": ["Purchase", "Signup"], "counts": [ {"date": "2024-01-01", "event": "Purchase", "count": 150}, {"date": "2024-01-01", "event": "Signup", "count": 45}, {"date": "2024-01-02", "event": "Purchase", "count": 175}, {"date": "2024-01-02", "event": "Signup", "count": 52} ] } ``` Examples: ``` mp inspect daily --from 2024-01-01 --to 2024-01-07 mp inspect daily --from 2024-01-01 --to 2024-01-07 -e Purchase,Signup ``` **jq Examples:** ``` --jq '.counts | [.[] | select(.event == "Purchase")] | map(.count) | add' # Total for one event --jq '.counts | [.[] | select(.date == "2024-01-01")]' # Counts for specific date --jq '.counts | [.[].date] | unique' # Get all dates --jq '.counts | group_by(.date) | [.[] | {date: .[0].date, total: map(.count) | add}]' # Daily totals ``` Usage: ``` mp inspect daily [OPTIONS] ``` Options: ``` --from TEXT Start date (YYYY-MM-DD). [required] --to TEXT End date (YYYY-MM-DD). [required] -e, --events TEXT Comma-separated event names (or all if omitted). -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### distribution Show property value distribution from Mixpanel. Uses JQL to count occurrences of each value for a property, showing counts and percentages sorted by frequency. Useful for understanding what values a property contains before writing queries. Output Structure (JSON): ``` { "event": "Purchase", "property_name": "country", "from_date": "2024-01-01", "to_date": "2024-01-31", "total_count": 50000, "values": [ {"value": "US", "count": 25000, "percentage": 50.0}, {"value": "UK", "count": 10000, "percentage": 20.0}, {"value": "DE", "count": 7500, "percentage": 15.0} ] } ``` Examples: ``` mp inspect distribution -e Purchase -p country --from 2024-01-01 --to 2024-01-31 mp inspect distribution -e Signup -p referrer --from 2024-01-01 --to 2024-01-31 --limit 10 ``` **jq Examples:** ``` --jq '.values | [.[].value]' # Get values only --jq '.values | [.[] | select(.percentage > 10)]' # Values with more than 10% --jq '.total_count' # Get total count --jq '.values[0]' # Get top value ``` Usage: ``` mp inspect distribution [OPTIONS] ``` Options: ``` -e, --event TEXT Event name to analyze. [required] -p, --property TEXT Property name to get distribution for. [required] --from TEXT Start date (YYYY-MM-DD). [required] --to TEXT End date (YYYY-MM-DD). [required] -l, --limit INTEGER Maximum values to return. [default: 20] -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### engagement Show user engagement distribution from Mixpanel. Uses JQL to bucket users by their event count, showing how many users performed N events. Useful for understanding user engagement levels. Output Structure (JSON): ``` { "from_date": "2024-01-01", "to_date": "2024-01-31", "events": null, "total_users": 8500, "buckets": [ {"bucket_min": 1, "bucket_label": "1", "user_count": 2500, "percentage": 29.4}, {"bucket_min": 2, "bucket_label": "2-5", "user_count": 3200, "percentage": 37.6}, {"bucket_min": 6, "bucket_label": "6-10", "user_count": 1800, "percentage": 21.2}, {"bucket_min": 11, "bucket_label": "11+", "user_count": 1000, "percentage": 11.8} ] } ``` Examples: ``` mp inspect engagement --from 2024-01-01 --to 2024-01-31 mp inspect engagement --from 2024-01-01 --to 2024-01-31 -e Purchase mp inspect engagement --from 2024-01-01 --to 2024-01-31 --buckets 1,5,10,50,100 ``` **jq Examples:** ``` --jq '.total_users' # Get total users --jq '.buckets | [.[] | select(.bucket_min >= 10)]' # Power users (high engagement) --jq '.buckets | .[] | select(.bucket_min == 1) | .percentage' # Single-event user percentage --jq '.buckets | [.[].bucket_label]' # Get bucket labels only ``` Usage: ``` mp inspect engagement [OPTIONS] ``` Options: ``` --from TEXT Start date (YYYY-MM-DD). [required] --to TEXT End date (YYYY-MM-DD). [required] -e, --events TEXT Comma-separated event names (or all if omitted). --buckets TEXT Comma-separated bucket boundaries (e.g., 1,5,10,50). -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### events List all event names from Mixpanel project. Calls the Mixpanel API to retrieve tracked event types. Use this to discover what events exist before querying. Output Structure (JSON): ``` ["Sign Up", "Login", "Purchase", "Page View", "Add to Cart"] ``` Examples: ``` mp inspect events mp inspect events --format table mp inspect events --format json --jq '.[0:3]' ``` **jq Examples:** ``` --jq '.[0:5]' # Get first 5 events --jq 'length' # Count total events --jq '[.[] | select(contains("Purchase"))]' # Find events containing "Purchase" --jq 'sort' # Sort alphabetically ``` Usage: ``` mp inspect events [OPTIONS] ``` Options: ``` -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### funnels List saved funnels in Mixpanel project. Calls the Mixpanel API to retrieve saved funnel definitions. Use the funnel_id with 'mp query funnel' to run funnel analysis. Output Structure (JSON): ``` [ {"funnel_id": 12345, "name": "Onboarding Flow"}, {"funnel_id": 12346, "name": "Purchase Funnel"}, {"funnel_id": 12347, "name": "Trial to Paid"} ] ``` Examples: ``` mp inspect funnels mp inspect funnels --format table ``` **jq Examples:** ``` --jq '[.[].funnel_id]' # Get all funnel IDs --jq '.[] | select(.name | test("Purchase"; "i"))' # Find funnel by name pattern --jq '[.[].name]' # Get funnel names only --jq 'length' # Count funnels ``` Usage: ``` mp inspect funnels [OPTIONS] ``` Options: ``` -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### lexicon-schema Get a single Lexicon schema from Mixpanel data dictionary. Retrieves the full schema definition for a specific event or profile property, including all property definitions and metadata. Output Structure (JSON): ``` { "entity_type": "event", "name": "Purchase", "schema_json": { "description": "User completed a purchase", "properties": { "amount": {"type": "number", "description": "Purchase amount in USD"}, "currency": {"type": "string", "description": "Currency code"}, "product_id": {"type": "string", "description": "Product identifier"} }, "metadata": {"hidden": false, "dropped": false, "tags": ["revenue"]} } } ``` Examples: ``` mp inspect lexicon-schema --type event --name "Purchase" mp inspect lexicon-schema -t event -n "Sign Up" mp inspect lexicon-schema -t profile -n "Plan Type" --format json ``` **jq Examples:** ``` --jq '.schema_json.properties | keys' # Get property names only --jq '.schema_json.properties | to_entries | [.[] | {name: .key, type: .value.type}]' # Get property types --jq '.schema_json.description' # Get description --jq '.schema_json.metadata.hidden' # Check if schema is hidden ``` Usage: ``` mp inspect lexicon-schema [OPTIONS] ``` Options: ``` -t, --type TEXT Entity type: event, profile, custom_event, etc. [required] -n, --name TEXT Entity name. [required] -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### lexicon-schemas List Lexicon schemas from Mixpanel data dictionary. Retrieves documented event and profile property schemas from the Mixpanel Lexicon. Shows schema names, types, and property counts. Output Structure (JSON): ``` [ {"entity_type": "event", "name": "Purchase", "property_count": 12, "description": "User completed purchase"}, {"entity_type": "event", "name": "Sign Up", "property_count": 8, "description": "New user registration"}, {"entity_type": "profile", "name": "Plan Type", "property_count": 3, "description": "User subscription tier"} ] ``` Examples: ``` mp inspect lexicon-schemas mp inspect lexicon-schemas --type event mp inspect lexicon-schemas --type profile --format table ``` **jq Examples:** ``` --jq '[.[] | select(.entity_type == "event")]' # Get only event schemas --jq '[.[].name]' # Get schema names --jq '[.[] | select(.property_count > 10)]' # Schemas with many properties --jq '[.[] | select(.description | test("purchase"; "i"))]' # Search by description ``` Usage: ``` mp inspect lexicon-schemas [OPTIONS] ``` Options: ``` -t, --type TEXT Entity type: event, profile, custom_event, etc. -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### numeric Show numeric property statistics from Mixpanel. Uses JQL to compute min, max, avg, stddev, and percentiles for a numeric property. Useful for understanding value ranges and distributions. Output Structure (JSON): ``` { "event": "Purchase", "property_name": "amount", "from_date": "2024-01-01", "to_date": "2024-01-31", "count": 5000, "min": 9.99, "max": 999.99, "sum": 125000.50, "avg": 25.00, "stddev": 45.75, "percentiles": {"25": 12.99, "50": 19.99, "75": 49.99, "90": 99.99} } ``` Examples: ``` mp inspect numeric -e Purchase -p amount --from 2024-01-01 --to 2024-01-31 mp inspect numeric -e Purchase -p amount --from 2024-01-01 --to 2024-01-31 --percentiles 10,50,90 ``` **jq Examples:** ``` --jq '.avg' # Get average value --jq '.percentiles["50"]' # Get median (50th percentile) --jq '{min, max}' # Get min and max --jq '.percentiles' # Get all percentiles ``` Usage: ``` mp inspect numeric [OPTIONS] ``` Options: ``` -e, --event TEXT Event name to analyze. [required] -p, --property TEXT Numeric property name. [required] --from TEXT Start date (YYYY-MM-DD). [required] --to TEXT End date (YYYY-MM-DD). [required] --percentiles TEXT Comma-separated percentiles (e.g., 25,50,75,90). -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### properties List properties for a specific event. Calls the Mixpanel API to retrieve property names tracked with an event. Shows both custom event properties and default Mixpanel properties. Output Structure (JSON): ``` ["country", "browser", "device", "$city", "$region", "plan_type"] ``` Examples: ``` mp inspect properties -e "Sign Up" mp inspect properties -e "Purchase" --format table ``` **jq Examples:** ``` --jq '.[0:10]' # Get first 10 properties --jq '[.[] | select(startswith("$") | not)]' # User-defined properties (no $ prefix) --jq '[.[] | select(startswith("$"))]' # Mixpanel system properties ($ prefix) --jq 'length' # Count properties ``` Usage: ``` mp inspect properties [OPTIONS] ``` Options: ``` -e, --event TEXT Event name. [required] -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### subproperties Discover subproperties of a list-of-object event property. Samples raw values via the Mixpanel property-values endpoint, parses each as JSON, and infers a scalar type per discovered subproperty. Use this when an event property like `cart` is a list of objects (e.g. `[{"Brand": "nike", "Price": 50}, ...]`) β€” the discovered subproperty names and types feed directly into `Filter.list_contains` and `GroupBy.list_item` (Python API). Output Structure (JSON): ``` [ {"name": "Brand", "type": "string", "sample_values": ["nike", "puma"]}, {"name": "Category", "type": "string", "sample_values": ["hats", "jeans"]}, {"name": "Price", "type": "number", "sample_values": [51, 87, 102]} ] ``` Examples: ``` mp inspect subproperties -p cart -e "Cart Viewed" mp inspect subproperties -p line_items -e Purchase --sample-size 100 mp inspect subproperties -p cart -e "Cart Viewed" --format table ``` **jq Examples:** ``` --jq '[.[] | select(.type == "number")]' # Numeric subproperties only --jq '[.[].name]' # Subproperty names only --jq '.[] | select(.name == "Brand")' # Find a specific subproperty --jq '[.[] | {name, type}]' # Drop sample_values ``` Usage: ``` mp inspect subproperties [OPTIONS] ``` Options: ``` -p, --property TEXT List-of-object property name. [required] -e, --event TEXT Event name (strongly recommended). -s, --sample-size INTEGER Number of raw values to sample. [default: 50] -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### top-events List today's top events by count. Calls the Mixpanel API to retrieve today's most frequent events. Useful for quick overview of project activity. Output Structure (JSON): ``` [ {"event": "Page View", "count": 15234, "percent_change": 12.5}, {"event": "Login", "count": 8921, "percent_change": -3.2}, {"event": "Purchase", "count": 1456, "percent_change": 8.7} ] ``` Examples: ``` mp inspect top-events mp inspect top-events --limit 20 --format table mp inspect top-events --type unique ``` **jq Examples:** ``` --jq '[.[] | select(.percent_change > 0)]' # Events with positive growth --jq '[.[].event]' # Get just event names --jq '[.[] | select(.count > 10000)]' # Events with count over 10000 --jq 'max_by(.percent_change)' # Event with highest growth ``` Usage: ``` mp inspect top-events [OPTIONS] ``` Options: ``` -t, --type TEXT Count type: general, unique, average. [default: general] -l, --limit INTEGER Maximum events to return. [default: 10] -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### values List sample values for a property. Calls the Mixpanel API to retrieve sample values for a property. Useful for understanding the data shape before writing queries. Output Structure (JSON): ``` ["US", "UK", "DE", "FR", "CA", "AU", "JP"] ``` Examples: ``` mp inspect values -p country mp inspect values -p country -e "Sign Up" --limit 20 mp inspect values -p browser --format table ``` **jq Examples:** ``` --jq '.[0:5]' # Get first 5 values --jq 'length' # Count unique values --jq '[.[] | select(test("^U"))]' # Filter values matching pattern --jq 'sort' # Sort values alphabetically ``` Usage: ``` mp inspect values [OPTIONS] ``` Options: ``` -p, --property TEXT Property name. [required] -e, --event TEXT Event name (optional). -l, --limit INTEGER Maximum values to return. [default: 100] -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` #### lexicon Manage Lexicon data definitions. Usage: ``` mp lexicon [OPTIONS] COMMAND [ARGS]... ``` ##### anomalies Manage data volume anomalies. Usage: ``` mp lexicon anomalies [OPTIONS] COMMAND [ARGS]... ``` ###### bulk-update Bulk-update data volume anomalies. Accepts a JSON string with parameters for updating multiple anomalies at once. Args: ctx: Typer context with global options. body: JSON string containing the bulk update payload. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp lexicon anomalies bulk-update [OPTIONS] ``` Options: ``` --body TEXT Bulk anomaly update payload as JSON string. [required] -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ###### list List data volume anomalies. Retrieves anomalies detected in data volume patterns. Optionally filter by status or limit the number of results. Args: ctx: Typer context with global options. status: Optional status filter (e.g. 'open', 'dismissed'). limit: Optional maximum number of results. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp lexicon anomalies list [OPTIONS] ``` Options: ``` --status TEXT Filter by anomaly status. --limit INTEGER Maximum number of anomalies to return. -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ###### update Update a single data volume anomaly. Changes the status and classification of a specific anomaly by ID. Args: ctx: Typer context with global options. id: Anomaly ID (integer). status: New status value. anomaly_class: Anomaly classification string. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp lexicon anomalies update [OPTIONS] ``` Options: ``` --id INTEGER Anomaly ID to update. [required] --status TEXT New anomaly status. [required] --anomaly-class TEXT Anomaly classification. [required] -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### audit Run schema audit to find violations. Audits the project schema for violations such as unexpected events, missing properties, or unexpected property types. Use `--events-only` for a faster, event-scoped audit. Args: ctx: Typer context with global options. events_only: If True, audit events only instead of full schema. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp lexicon audit [OPTIONS] ``` Options: ``` --events-only Run audit for events only (faster, fewer results). -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### deletion-requests Manage event deletion requests. Usage: ``` mp lexicon deletion-requests [OPTIONS] COMMAND [ARGS]... ``` ###### cancel Cancel a pending deletion request. Cancels a previously submitted deletion request by its ID. Only pending requests can be cancelled. Args: ctx: Typer context with global options. id: Deletion request ID (positional argument). format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp lexicon deletion-requests cancel [OPTIONS] ID ``` Options: ``` ID Deletion request ID to cancel. [required] -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ###### create Create an event deletion request. Submits a request to delete event data matching the specified event name, date range, and optional property filters. Args: ctx: Typer context with global options. event_name: Event name to delete data for. from_date: Start date in YYYY-MM-DD format. to_date: End date in YYYY-MM-DD format. filters: Optional JSON string with property filters. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp lexicon deletion-requests create [OPTIONS] ``` Options: ``` --event-name TEXT Event name to delete data for. [required] --from-date TEXT Start date (YYYY-MM-DD). [required] --to-date TEXT End date (YYYY-MM-DD). [required] --filters TEXT Optional property filters as JSON string. -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ###### list List event deletion requests. Retrieves all event deletion requests for the project. Args: ctx: Typer context with global options. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp lexicon deletion-requests list [OPTIONS] ``` Options: ``` -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ###### preview Preview deletion filter results. Shows what data would be affected by a deletion request without actually creating one. Useful for validating filters before submitting a real deletion request. Args: ctx: Typer context with global options. event_name: Event name to preview deletion for. from_date: Start date in YYYY-MM-DD format. to_date: End date in YYYY-MM-DD format. filters: Optional JSON string with property filters. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp lexicon deletion-requests preview [OPTIONS] ``` Options: ``` --event-name TEXT Event name to preview deletion for. [required] --from-date TEXT Start date (YYYY-MM-DD). [required] --to-date TEXT End date (YYYY-MM-DD). [required] --filters TEXT Optional property filters as JSON string. -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### enforcement Manage schema enforcement. Usage: ``` mp lexicon enforcement [OPTIONS] COMMAND [ARGS]... ``` ###### delete Delete schema enforcement settings. Removes all schema enforcement configuration for the project. This action cannot be undone. Args: ctx: Typer context with global options. Usage: ``` mp lexicon enforcement delete [OPTIONS] ``` ###### get Get schema enforcement settings. Retrieves the current schema enforcement configuration for the project. Use `--fields` to limit the response to specific fields. Args: ctx: Typer context with global options. fields: Optional comma-separated field names to include. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp lexicon enforcement get [OPTIONS] ``` Options: ``` --fields TEXT Comma-separated fields to include. -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ###### init Initialize schema enforcement. Sets up schema enforcement for the project with the given rule event as the initial enforcement target. Args: ctx: Typer context with global options. rule_event: Enforcement action ("Warn and Accept", "Warn and Hide", or "Warn and Drop"). format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp lexicon enforcement init [OPTIONS] ``` Options: ``` --rule-event TEXT Enforcement action (e.g. 'Warn and Accept', 'Warn and Hide', 'Warn and Drop'). [required] -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ###### replace Replace schema enforcement settings (PUT semantics). Replaces the entire schema enforcement configuration with the provided payload. All existing settings will be overwritten. Args: ctx: Typer context with global options. body: JSON string containing the full replacement payload. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp lexicon enforcement replace [OPTIONS] ``` Options: ``` --body TEXT Schema enforcement replacement payload as JSON string. [required] -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ###### update Update schema enforcement settings (PATCH semantics). Applies a partial update to the schema enforcement configuration. Accepts a JSON string with the fields to update. Args: ctx: Typer context with global options. body: JSON string containing the update payload. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp lexicon enforcement update [OPTIONS] ``` Options: ``` --body TEXT Schema enforcement update payload as JSON string. [required] -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### event-history Get change history for an event definition. Retrieves a chronological list of changes made to the event definition over time. Args: ctx: Typer context with global options. event_name: Event name. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp lexicon event-history [OPTIONS] ``` Options: ``` --event-name TEXT Event name to get history for. [required] -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### events Manage event definitions. Usage: ``` mp lexicon events [OPTIONS] COMMAND [ARGS]... ``` ###### bulk-update Bulk-update event definitions. Accepts a JSON string with a list of event updates. Each entry should include `name` and any fields to change. Args: ctx: Typer context with global options. data: JSON string containing bulk update payload. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp lexicon events bulk-update [OPTIONS] ``` Options: ``` --data TEXT Bulk update payload as JSON string (list of event updates). [required] -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ###### delete Delete an event definition from Lexicon. Permanently removes the event definition. This action cannot be undone. Args: ctx: Typer context with global options. name: Event name to delete. Usage: ``` mp lexicon events delete [OPTIONS] ``` Options: ``` --name TEXT Event name to delete. [required] ``` ###### get Get event definitions by name. Retrieves metadata (description, tags, visibility, etc.) for the specified events from the Mixpanel Lexicon. Args: ctx: Typer context with global options. names: Comma-separated event names. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp lexicon events get [OPTIONS] ``` Options: ``` --names TEXT Comma-separated event names. [required] -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ###### update Update an event definition (PATCH semantics). Only fields provided will be updated. Use boolean flags like `--hidden/--no-hidden` to toggle visibility. Args: ctx: Typer context with global options. name: Event name to update. hidden: Whether to hide the event. dropped: Whether to drop event data at ingestion. verified: Whether to mark the event as verified. description: New description text. tags: Comma-separated tag names to assign. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp lexicon events update [OPTIONS] ``` Options: ``` --name TEXT Event name to update. [required] --hidden / --no-hidden Hide or unhide event. --dropped / --no-dropped Drop or undrop event data. --verified / --no-verified Mark event as verified or unverified. --description TEXT New event description. --tags TEXT Comma-separated tag names to assign. -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### export Export Lexicon data definitions. Exports event and property definitions, optionally filtered by type. Args: ctx: Typer context with global options. types: Comma-separated export types to include. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp lexicon export [OPTIONS] ``` Options: ``` --types TEXT Comma-separated export types (events, event_properties, user_properties). -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### properties Manage property definitions. Usage: ``` mp lexicon properties [OPTIONS] COMMAND [ARGS]... ``` ###### bulk-update Bulk-update property definitions. Accepts a JSON string with a list of property updates. Each entry should include `name` and any fields to change. Args: ctx: Typer context with global options. data: JSON string containing bulk update payload. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp lexicon properties bulk-update [OPTIONS] ``` Options: ``` --data TEXT Bulk update payload as JSON string (list of property updates). [required] -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ###### get Get property definitions by name. Retrieves metadata (description, tags, visibility, etc.) for the specified properties from the Mixpanel Lexicon. Args: ctx: Typer context with global options. names: Comma-separated property names. resource_type: Optional resource type filter. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp lexicon properties get [OPTIONS] ``` Options: ``` --names TEXT Comma-separated property names. [required] --resource-type TEXT Resource type filter (event, user, groupprofile). -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ###### update Update a property definition (PATCH semantics). Only fields provided will be updated. Use boolean flags like `--hidden/--no-hidden` to toggle visibility. Args: ctx: Typer context with global options. name: Property name to update. hidden: Whether to hide the property. dropped: Whether to drop property data. sensitive: Whether to mark the property as PII sensitive. description: New description text. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp lexicon properties update [OPTIONS] ``` Options: ``` --name TEXT Property name to update. [required] --hidden / --no-hidden Hide or unhide property. --dropped / --no-dropped Drop or undrop property data. --sensitive / --no-sensitive Mark property as PII sensitive. --description TEXT New property description. -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### property-history Get change history for a property definition. Retrieves a chronological list of changes made to the property definition over time. Args: ctx: Typer context with global options. property_name: Property name. entity_type: Entity type (event, user, group). format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp lexicon property-history [OPTIONS] ``` Options: ``` --property-name TEXT Property name to get history for. [required] --entity-type TEXT Entity type (event, user, group). [required] -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### tags Manage Lexicon tags. Usage: ``` mp lexicon tags [OPTIONS] COMMAND [ARGS]... ``` ###### create Create a new Lexicon tag. Creates a tag that can be assigned to event and property definitions. Args: ctx: Typer context with global options. name: Tag name to create. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp lexicon tags create [OPTIONS] ``` Options: ``` --name TEXT Tag name to create. [required] -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ###### delete Delete a Lexicon tag by name. Permanently removes the tag. This action cannot be undone. Args: ctx: Typer context with global options. name: Tag name to delete. Usage: ``` mp lexicon tags delete [OPTIONS] ``` Options: ``` --name TEXT Tag name to delete. [required] ``` ###### list List all Lexicon tags. Retrieves all tags available for categorizing event and property definitions. Args: ctx: Typer context with global options. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp lexicon tags list [OPTIONS] ``` Options: ``` -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ###### update Update an existing Lexicon tag. Renames an existing tag by its ID. Args: ctx: Typer context with global options. id: Tag ID (integer). name: New tag name. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp lexicon tags update [OPTIONS] ``` Options: ``` --id INTEGER Tag ID to update. [required] --name TEXT New tag name. [required] -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### tracking-metadata Get tracking metadata for an event. Retrieves information about how an event is being tracked (sources, SDKs, volume, etc.). Args: ctx: Typer context with global options. event_name: Event name. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp lexicon tracking-metadata [OPTIONS] ``` Options: ``` --event-name TEXT Event name to get metadata for. [required] -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` #### login Add a Mixpanel account with guided region / project / name resolution. Usage: ``` mp login [OPTIONS] ``` Options: ``` --name TEXT Local account name. Wins over derived names. --region TEXT Force a specific region (us | eu | in). --project TEXT Project ID to bind to the new account. Hard-fails if not visible. -S, --service-account Force the service_account auth path. --token-env TEXT Force oauth_token auth from the named env var (e.g. --token-env MY_TOKEN). When omitted, MP_OAUTH_TOKEN is consulted automatically. --no-browser For oauth_browser, print the authorization URL instead of opening the browser. --secret-stdin For service_account, read the secret from stdin. ``` #### lookup-tables Manage lookup tables. Usage: ``` mp lookup-tables [OPTIONS] COMMAND [ARGS]... ``` ##### delete Delete one or more lookup tables. Permanently deletes lookup tables by their data group IDs. Provide IDs as a comma-separated string. Args: ctx: Typer context with global options. data_group_ids: Comma-separated data group IDs. Usage: ``` mp lookup-tables delete [OPTIONS] ``` Options: ``` --data-group-ids TEXT Comma-separated data group IDs to delete (required). [required] ``` ##### download Download lookup table data as CSV. Downloads the lookup table data. If --output is specified, writes the CSV to the given file path. Otherwise, prints CSV to stdout. Args: ctx: Typer context with global options. data_group_id: Data group ID of the lookup table. file_name: Optional file name filter. limit: Optional row limit. output: Output file path. Usage: ``` mp lookup-tables download [OPTIONS] ``` Options: ``` --data-group-id INTEGER Data group ID (required). [required] --file-name TEXT Optional file name filter. --limit INTEGER Optional row limit. --output TEXT Output file path (writes CSV to file). ``` ##### download-url Get a signed download URL for a lookup table. Returns a signed URL that can be used to download the lookup table data directly. Args: ctx: Typer context with global options. data_group_id: Data group ID of the lookup table. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp lookup-tables download-url [OPTIONS] ``` Options: ``` --data-group-id INTEGER Data group ID (required). [required] -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### list List lookup tables for the current project. Retrieves all lookup tables, optionally filtered by data group ID. Args: ctx: Typer context with global options. data_group_id: Optional filter by data group ID. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp lookup-tables list [OPTIONS] ``` Options: ``` --data-group-id INTEGER Filter by data group ID. -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### update Update a lookup table. Updates the name of an existing lookup table identified by its data group ID. Args: ctx: Typer context with global options. data_group_id: Data group ID of the lookup table. name: New table name. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp lookup-tables update [OPTIONS] ``` Options: ``` --data-group-id INTEGER Data group ID (required). [required] --name TEXT New table name (required). [required] -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### upload Upload a CSV file as a new lookup table. Performs a 3-step upload: obtains a signed URL, uploads the CSV, then registers the table. Args: ctx: Typer context with global options. name: Table name. file: Path to the local CSV file. data_group_id: Data group ID for replacing an existing table. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp lookup-tables upload [OPTIONS] ``` Options: ``` --name TEXT Table name (required). [required] --file TEXT Path to CSV file to upload (required). [required] --data-group-id INTEGER Data group ID (for replacing an existing table). -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### upload-url Get a signed URL for uploading lookup table data. Returns a signed upload URL, path, and key that can be used to upload data directly. Args: ctx: Typer context with global options. content_type: MIME type of the file to upload. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp lookup-tables upload-url [OPTIONS] ``` Options: ``` --content-type TEXT MIME type of the file. [default: text/csv] -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` #### project Active project. Usage: ``` mp project [OPTIONS] COMMAND [ARGS]... ``` ##### list List projects accessible by the active account. Always enumerates from `/me` (24h cached). The active project is marked. `--refresh` bypasses the local cache and refetches. Works with only authentication configured β€” no project axis required (FR-047). Args: ctx: Typer context. refresh: Bypass the local /me cache and refetch. format: Output format (`table` / `json` / `jsonl`). Usage: ``` mp project list [OPTIONS] ``` Options: ``` --refresh Bypass the local /me cache and refetch. -f, --format TEXT Output format: table | json | jsonl. [default: table] ``` ##### show Show the currently active project (active account's `default_project`). Args: ctx: Typer context. Usage: ``` mp project show [OPTIONS] ``` ##### use Update the active account's `default_project`. Project lives on the account, not in `[active]`. This command is equivalent to `mp account update --project ID`. Args: ctx: Typer context. project_id: Mixpanel project ID (numeric string). Usage: ``` mp project use [OPTIONS] PROJECT_ID ``` Options: ``` PROJECT_ID Numeric Mixpanel project ID. [required] ``` #### query Query Mixpanel data. Usage: ``` mp query [OPTIONS] COMMAND [ARGS]... ``` ##### activity-feed Query user activity feed for specific users. Retrieves the event history for one or more users identified by their distinct_id. Pass comma-separated IDs to --users. Optionally filter by date range with --from and --to. Without date filters, returns recent activity (API default). **Output Structure (JSON):** ``` { "distinct_ids": ["user123", "user456"], "from_date": "2025-01-01", "to_date": "2025-01-31", "event_count": 47, "events": [ { "event": "Login", "time": "2025-01-15T10:30:00+00:00", "properties": {"$browser": "Chrome", "$city": "San Francisco", ...} }, { "event": "Purchase", "time": "2025-01-15T11:45:00+00:00", "properties": {"product_id": "SKU123", "amount": 99.99, ...} } ] } ``` **Examples:** ``` mp query activity-feed --users "user123" mp query activity-feed --users "user123,user456" --from 2025-01-01 --to 2025-01-31 mp query activity-feed --users "user123" --format table ``` **jq Examples:** ``` --jq '.event_count' # Total number of events --jq '.events | length' # Same as above --jq '.events[].event' # List all event names --jq '.events | group_by(.event) | map({event: .[0].event, count: length})' ``` Usage: ``` mp query activity-feed [OPTIONS] ``` Options: ``` -U, --users TEXT Comma-separated distinct IDs. [required] --from TEXT Start date (YYYY-MM-DD). --to TEXT End date (YYYY-MM-DD). -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### event-counts Query event counts over time for multiple events. Compares multiple events on the same time series. Pass comma-separated event names to --events (e.g., --events "Sign Up,Login,Purchase"). The --type option controls how counts are calculated: - general: Total event occurrences (default) - unique: Unique users who triggered the event - average: Average events per user **Output Structure (JSON):** ``` { "events": ["Sign Up", "Login", "Purchase"], "from_date": "2025-01-01", "to_date": "2025-01-07", "unit": "day", "type": "general", "series": { "Sign Up": {"2025-01-01": 150, "2025-01-02": 175, ...}, "Login": {"2025-01-01": 520, "2025-01-02": 610, ...}, "Purchase": {"2025-01-01": 45, "2025-01-02": 52, ...} } } ``` **Examples:** ``` mp query event-counts --events "Sign Up,Login,Purchase" --from 2025-01-01 --to 2025-01-31 mp query event-counts --events "Sign Up,Purchase" --from 2025-01-01 --to 2025-01-31 --type unique mp query event-counts --events "Login" --from 2025-01-01 --to 2025-01-31 --unit week ``` **jq Examples:** ``` --jq '.series | keys' # List event names --jq '.series["Login"] | add' # Sum counts for one event --jq '.series["Login"]["2025-01-01"]' # Count for specific date --jq '[.series | to_entries[] | {event: .key, total: (.value | add)}]' ``` Usage: ``` mp query event-counts [OPTIONS] ``` Options: ``` -e, --events TEXT Comma-separated event names. [required] --from TEXT Start date (YYYY-MM-DD). [required] --to TEXT End date (YYYY-MM-DD). [required] -t, --type TEXT Count type: general, unique, average. [default: general] -u, --unit TEXT Time unit: day, week, month. [default: day] -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### flows Query a saved Flows report by bookmark ID. Retrieves data from a saved Flows report in Mixpanel. The bookmark_id can be found in the URL when viewing a flows report (the numeric ID after /flows/). Flows reports show user paths through a sequence of events with step-by-step conversion rates and path breakdowns. **Output Structure (JSON):** ``` { "bookmark_id": 12345, "computed_at": "2025-01-15T10:30:00Z", "steps": [ {"step": 1, "event": "Sign Up", "count": 10000}, {"step": 2, "event": "Verify Email", "count": 7500}, {"step": 3, "event": "Complete Profile", "count": 4200} ], "breakdowns": [ {"path": ["Sign Up", "Verify Email", "Complete Profile"], "count": 3800}, {"path": ["Sign Up", "Verify Email", "Drop Off"], "count": 3300} ], "overall_conversion_rate": 0.42, "metadata": {...} } ``` **Examples:** ``` mp query flows 12345 mp query flows 12345 --format table ``` **jq Examples:** ``` --jq '.overall_conversion_rate' # End-to-end conversion rate --jq '.steps | length' # Number of flow steps --jq '.steps[] | {event, count}' # Event and count per step --jq '.breakdowns | sort_by(.count) | reverse | .[0]' ``` Usage: ``` mp query flows [OPTIONS] BOOKMARK_ID ``` Options: ``` BOOKMARK_ID Saved flows report bookmark ID. [required] -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### frequency Analyze event frequency distribution (addiction analysis). Shows how many users performed an event N times within each time period. Useful for understanding user engagement depth and "power user" distribution. The --addiction-unit controls granularity of frequency buckets (hour or day). For example, with --addiction-unit hour, the data shows how many users performed the event 1 time, 2 times, 3 times, etc. per hour. **Output Structure (JSON):** ``` { "event": "Login", "from_date": "2025-01-01", "to_date": "2025-01-07", "unit": "day", "addiction_unit": "hour", "data": { "2025-01-01": [500, 250, 125, 60, 30, 15], "2025-01-02": [520, 260, 130, 65, 32, 16], ... } } ``` Each array shows user counts by frequency (index 0 = 1x, index 1 = 2x, etc.). **Examples:** ``` mp query frequency --from 2025-01-01 --to 2025-01-31 mp query frequency -e "Login" --from 2025-01-01 --to 2025-01-31 mp query frequency -e "Login" --from 2025-01-01 --to 2025-01-31 --addiction-unit day ``` **jq Examples:** ``` --jq '.data | keys' # List all dates --jq '.data["2025-01-01"][0]' # Users who did it once on Jan 1 --jq '.data["2025-01-01"] | add' # Total active users on Jan 1 --jq '.data | to_entries | map({date: .key, power_users: .value[4:] | add})' ``` Usage: ``` mp query frequency [OPTIONS] ``` Options: ``` --from TEXT Start date (YYYY-MM-DD). [required] --to TEXT End date (YYYY-MM-DD). [required] -e, --event TEXT Event name (all events if omitted). -u, --unit TEXT Time unit: day, week, month. [default: day] --addiction-unit TEXT Addiction unit: hour, day. [default: hour] -w, --where TEXT Filter expression. -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### funnel Run live funnel analysis against Mixpanel API. Analyzes conversion through a saved funnel's steps. The funnel_id can be found in the Mixpanel UI URL when viewing the funnel, or via 'mp inspect funnels'. **Output Structure (JSON):** ``` { "funnel_id": 12345, "funnel_name": "Onboarding Funnel", "from_date": "2025-01-01", "to_date": "2025-01-31", "conversion_rate": 0.23, "steps": [ {"event": "Sign Up", "count": 10000, "conversion_rate": 1.0}, {"event": "Verify Email", "count": 7500, "conversion_rate": 0.75}, {"event": "Complete Profile", "count": 4200, "conversion_rate": 0.56}, {"event": "First Purchase", "count": 2300, "conversion_rate": 0.55} ] } ``` **Examples:** ``` mp query funnel 12345 --from 2025-01-01 --to 2025-01-31 mp query funnel 12345 --from 2025-01-01 --to 2025-01-31 --unit week mp query funnel 12345 --from 2025-01-01 --to 2025-01-31 --on country ``` **jq Examples:** ``` --jq '.conversion_rate' # Overall conversion rate --jq '.steps | length' # Number of funnel steps --jq '.steps[-1].count' # Users completing the funnel --jq '.steps[] | {event, rate: .conversion_rate}' ``` Usage: ``` mp query funnel [OPTIONS] FUNNEL_ID ``` Options: ``` FUNNEL_ID Funnel ID. [required] --from TEXT Start date (YYYY-MM-DD). [required] --to TEXT End date (YYYY-MM-DD). [required] -u, --unit TEXT Time unit: day, week, month. -o, --on TEXT Property to segment by. -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### jql Execute JQL script against Mixpanel API. Script can be provided as a file argument or inline with --script. Parameters can be passed with --param key=value (repeatable). **Output Structure (JSON):** The output structure depends on your JQL script. Common patterns: groupBy result: ``` { "raw": [ {"key": ["Login"], "value": 5234}, {"key": ["Sign Up"], "value": 1892} ], "row_count": 2 } ``` Aggregation result: ``` { "raw": [{"count": 15234, "unique_users": 3421}], "row_count": 1 } ``` **Examples:** ``` mp query jql analysis.js mp query jql --script "function main() { return Events({...}).groupBy(['event'], mixpanel.reducer.count()) }" mp query jql analysis.js --param start_date=2025-01-01 --param event_name=Login ``` **jq Examples:** ``` --jq '.raw' # Get raw result array --jq '.raw[0]' # First result row --jq '.raw[] | {event: .key[0], count: .value}' --jq '.row_count' # Number of result rows ``` Usage: ``` mp query jql [OPTIONS] [FILE] ``` Options: ``` [FILE] JQL script file. -c, --script TEXT Inline JQL script. -P, --param TEXT Parameter (key=value). -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### property-counts Query event counts broken down by property values. Shows how event counts vary across different values of a property. For example, --property country shows event counts per country. The --type option controls how counts are calculated: - general: Total event occurrences (default) - unique: Unique users who triggered the event - average: Average events per user The --limit option controls how many property values to return (default 10, ordered by count descending). **Output Structure (JSON):** ``` { "event": "Purchase", "property_name": "country", "from_date": "2025-01-01", "to_date": "2025-01-07", "unit": "day", "type": "general", "series": { "US": {"2025-01-01": 150, "2025-01-02": 175, ...}, "UK": {"2025-01-01": 75, "2025-01-02": 80, ...}, "DE": {"2025-01-01": 45, "2025-01-02": 52, ...} } } ``` **Examples:** ``` mp query property-counts -e "Purchase" -p country --from 2025-01-01 --to 2025-01-31 mp query property-counts -e "Sign Up" -p "utm_source" --from 2025-01-01 --to 2025-01-31 --limit 20 mp query property-counts -e "Login" -p browser --from 2025-01-01 --to 2025-01-31 --type unique ``` **jq Examples:** ``` --jq '.series | keys' # List property values --jq '.series["US"] | add' # Sum counts for one value --jq '.series | to_entries | sort_by(.value | add) | reverse' --jq '[.series | to_entries[] | {value: .key, total: (.value | add)}]' ``` Usage: ``` mp query property-counts [OPTIONS] ``` Options: ``` -e, --event TEXT Event name. [required] -p, --property TEXT Property name. [required] --from TEXT Start date (YYYY-MM-DD). [required] --to TEXT End date (YYYY-MM-DD). [required] -t, --type TEXT Count type: general, unique, average. [default: general] -u, --unit TEXT Time unit: day, week, month. [default: day] -l, --limit INTEGER Max property values to return. [default: 10] -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### retention Run live retention analysis against Mixpanel API. Measures how many users return after their first action (birth event). Users are grouped into cohorts by when they first did the birth event, then tracked for how many returned to do the return event. The --interval and --intervals options control bucket granularity: --interval is the bucket size (default 1), --intervals is the number of buckets to track (default 10). Combined with --unit, this defines the retention window (e.g., --unit day --interval 1 --intervals 7 tracks daily retention for 7 days). **Output Structure (JSON):** ``` { "born_event": "Sign Up", "return_event": "Login", "from_date": "2025-01-01", "to_date": "2025-01-31", "unit": "day", "cohorts": [ {"date": "2025-01-01", "size": 500, "retention": [1.0, 0.65, 0.45, 0.38]}, {"date": "2025-01-02", "size": 480, "retention": [1.0, 0.62, 0.41, 0.35]}, {"date": "2025-01-03", "size": 520, "retention": [1.0, 0.68, 0.48, 0.40]} ] } ``` **Examples:** ``` mp query retention --born "Sign Up" --return "Login" --from 2025-01-01 --to 2025-01-31 mp query retention --born "Sign Up" --return "Purchase" --from 2025-01-01 --to 2025-01-31 --unit week mp query retention --born "Sign Up" --return "Login" --from 2025-01-01 --to 2025-01-31 --intervals 7 ``` **jq Examples:** ``` --jq '.cohorts | length' # Number of cohorts --jq '.cohorts[0].retention' # First cohort retention curve --jq '.cohorts[] | {date, size, day7: .retention[7]}' ``` Usage: ``` mp query retention [OPTIONS] ``` Options: ``` -b, --born TEXT Birth event. [required] -r, --return TEXT Return event. [required] --from TEXT Start date (YYYY-MM-DD). [required] --to TEXT End date (YYYY-MM-DD). [required] --born-where TEXT Birth event filter. --return-where TEXT Return event filter. -i, --interval INTEGER Bucket size. -n, --intervals INTEGER Number of buckets. -u, --unit TEXT Time unit: day, week, month. [default: day] -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### saved-report Query a saved report (Insights, Retention, or Funnel) by bookmark ID. Retrieves data from a saved report in Mixpanel. The bookmark_id can be found in the URL when viewing a report (the numeric ID after /insights/, /retention/, or /funnels/). The report type is automatically detected from the response headers. **Output Structure (JSON):** Insights report: ``` { "bookmark_id": 12345, "computed_at": "2025-01-15T10:30:00Z", "from_date": "2025-01-01", "to_date": "2025-01-31", "headers": ["$event"], "series": { "Sign Up": {"2025-01-01": 150, "2025-01-02": 175, ...}, "Login": {"2025-01-01": 520, "2025-01-02": 610, ...} }, "report_type": "insights" } ``` Funnel/Retention reports have different series structures based on the saved report configuration. **Examples:** ``` mp query saved-report 12345 mp query saved-report 12345 --format table ``` **jq Examples:** ``` --jq '.report_type' # Report type (insights/retention/funnel) --jq '.series | keys' # List series names --jq '.headers' # Report column headers --jq '.series | to_entries | map({name: .key, total: (.value | add)})' ``` Usage: ``` mp query saved-report [OPTIONS] BOOKMARK_ID ``` Options: ``` BOOKMARK_ID Saved report bookmark ID. [required] -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### segmentation Run live segmentation query against Mixpanel API. Returns time-series event counts, optionally segmented by a property. Without --on, returns total counts per time period. With --on, breaks down counts by property values (e.g., --on country shows counts per country). The --on parameter accepts bare property names (e.g., 'country') or full filter expressions (e.g., 'properties["country"] == "US"'). **Output Structure (JSON):** ``` { "event": "Sign Up", "from_date": "2025-01-01", "to_date": "2025-01-07", "unit": "day", "segment_property": "country", "total": 1850, "series": { "US": {"2025-01-01": 150, "2025-01-02": 175, ...}, "UK": {"2025-01-01": 75, "2025-01-02": 80, ...} } } ``` **Examples:** ``` mp query segmentation -e "Sign Up" --from 2025-01-01 --to 2025-01-31 mp query segmentation -e "Purchase" --from 2025-01-01 --to 2025-01-31 --on country mp query segmentation -e "Login" --from 2025-01-01 --to 2025-01-07 --unit week ``` **jq Examples:** ``` --jq '.total' # Total event count --jq '.series | keys' # List segment names --jq '.series["US"] | add' # Sum counts for one segment ``` Usage: ``` mp query segmentation [OPTIONS] ``` Options: ``` -e, --event TEXT Event name. [required] --from TEXT Start date (YYYY-MM-DD). [required] --to TEXT End date (YYYY-MM-DD). [required] -o, --on TEXT Property to segment by (bare name or expression). -u, --unit TEXT Time unit: day, week, month. [default: day] -w, --where TEXT Filter expression. -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### segmentation-average Calculate average of numeric property over time. Calculates the mean value of a numeric property across all matching events. Useful for tracking averages like order value, session duration, or scores. For example, --event Purchase --on order_value calculates average order value per time period. **Output Structure (JSON):** ``` { "event": "Purchase", "from_date": "2025-01-01", "to_date": "2025-01-07", "property_expr": "order_value", "unit": "day", "results": { "2025-01-01": 85.50, "2025-01-02": 92.75, "2025-01-03": 78.25, ... } } ``` **Examples:** ``` mp query segmentation-average -e "Purchase" --on order_value --from 2025-01-01 --to 2025-01-31 mp query segmentation-average -e "Session" --on duration --from 2025-01-01 --to 2025-01-31 --unit hour ``` **jq Examples:** ``` --jq '.results | add / length' # Overall average --jq '.results | to_entries | max_by(.value)' # Highest day --jq '.results | to_entries | min_by(.value)' # Lowest day --jq '[.results | to_entries[] | {date: .key, avg: .value}]' ``` Usage: ``` mp query segmentation-average [OPTIONS] ``` Options: ``` -e, --event TEXT Event name. [required] -o, --on TEXT Numeric property to average (bare name or expression). [required] --from TEXT Start date (YYYY-MM-DD). [required] --to TEXT End date (YYYY-MM-DD). [required] -u, --unit TEXT Time unit: hour, day. [default: day] -w, --where TEXT Filter expression. -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### segmentation-numeric Bucket events by numeric property ranges. Groups events into buckets based on a numeric property's value. Mixpanel automatically determines optimal bucket ranges based on the property's value distribution. For example, --on price might create buckets like "0-10", "10-50", "50+". The --type option controls how counts are calculated: - general: Total event occurrences (default) - unique: Unique users who triggered the event - average: Average events per user **Output Structure (JSON):** ``` { "event": "Purchase", "from_date": "2025-01-01", "to_date": "2025-01-07", "property_expr": "amount", "unit": "day", "series": { "0-50": {"2025-01-01": 120, "2025-01-02": 135, ...}, "50-100": {"2025-01-01": 85, "2025-01-02": 92, ...}, "100-500": {"2025-01-01": 45, "2025-01-02": 52, ...}, "500+": {"2025-01-01": 12, "2025-01-02": 15, ...} } } ``` **Examples:** ``` mp query segmentation-numeric -e "Purchase" --on amount --from 2025-01-01 --to 2025-01-31 mp query segmentation-numeric -e "Purchase" --on amount --from 2025-01-01 --to 2025-01-31 --type unique ``` **jq Examples:** ``` --jq '.series | keys' # List bucket ranges --jq '.series["100-500"] | add' # Sum counts for a bucket --jq '[.series | to_entries[] | {bucket: .key, total: (.value | add)}]' --jq '.series | to_entries | sort_by(.value | add) | reverse' ``` Usage: ``` mp query segmentation-numeric [OPTIONS] ``` Options: ``` -e, --event TEXT Event name. [required] -o, --on TEXT Numeric property to bucket (bare name or expression). [required] --from TEXT Start date (YYYY-MM-DD). [required] --to TEXT End date (YYYY-MM-DD). [required] -t, --type TEXT Count type: general, unique, average. [default: general] -u, --unit TEXT Time unit: hour, day. [default: day] -w, --where TEXT Filter expression. -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### segmentation-sum Calculate sum of numeric property over time. Sums the values of a numeric property across all matching events. Useful for tracking totals like revenue, quantity, or duration. For example, --event Purchase --on revenue calculates total revenue per time period. **Output Structure (JSON):** ``` { "event": "Purchase", "from_date": "2025-01-01", "to_date": "2025-01-07", "property_expr": "revenue", "unit": "day", "results": { "2025-01-01": 15234.50, "2025-01-02": 18456.75, "2025-01-03": 12890.25, ... } } ``` **Examples:** ``` mp query segmentation-sum -e "Purchase" --on revenue --from 2025-01-01 --to 2025-01-31 mp query segmentation-sum -e "Purchase" --on quantity --from 2025-01-01 --to 2025-01-31 --unit hour ``` **jq Examples:** ``` --jq '.results | add' # Total sum across all dates --jq '.results | to_entries | max_by(.value)' # Highest day --jq '.results | to_entries | min_by(.value)' # Lowest day --jq '[.results | to_entries[] | {date: .key, revenue: .value}]' ``` Usage: ``` mp query segmentation-sum [OPTIONS] ``` Options: ``` -e, --event TEXT Event name. [required] -o, --on TEXT Numeric property to sum (bare name or expression). [required] --from TEXT Start date (YYYY-MM-DD). [required] --to TEXT End date (YYYY-MM-DD). [required] -u, --unit TEXT Time unit: hour, day. [default: day] -w, --where TEXT Filter expression. -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` #### reports Manage Mixpanel reports (bookmarks). Usage: ``` mp reports [OPTIONS] COMMAND [ARGS]... ``` ##### bulk-delete Delete multiple bookmarks at once. Permanently removes all specified bookmarks from the project. Args: ctx: Typer context with global options. ids: Comma-separated bookmark IDs to delete. Example: ``` mp reports bulk-delete --ids 1,2,3 ``` Usage: ``` mp reports bulk-delete [OPTIONS] ``` Options: ``` --ids TEXT Comma-separated list of bookmark IDs to delete. [required] ``` ##### bulk-update Update multiple bookmarks at once. Accepts a JSON array of update entries. Each entry must include an `id` field and any fields to update (e.g., `name`). Args: ctx: Typer context with global options. entries: JSON string containing a list of update entries. Example: ``` mp reports bulk-update --entries '[{"id": 1, "name": "Renamed"}]' ``` Usage: ``` mp reports bulk-update [OPTIONS] ``` Options: ``` -e, --entries TEXT JSON string: list of objects with "id" and fields to update. [required] ``` ##### create Create a new bookmark (saved report). Creates a bookmark in the Mixpanel App API with the given name, type, and query parameters. Args: ctx: Typer context with global options. name: Name for the new report. bookmark_type: Report type (e.g., `"insights"`, `"funnels"`). params: Report parameters as a JSON string. description: Optional description for the report. dashboard_id: Optional dashboard ID to add the report to. format: Output format (json, jsonl, table, csv, plain). jq_filter: Optional jq filter expression for JSON output. Example: ``` mp reports create --name "Signup Funnel" --type funnels \ --params '{"events": [{"event": "Signup"}]}' ``` Usage: ``` mp reports create [OPTIONS] ``` Options: ``` -n, --name TEXT Name for the new report. [required] -t, --type TEXT Report type (e.g., insights, funnels). [required] -p, --params TEXT Report parameters as a JSON string. [required] -d, --description TEXT Optional description. --dashboard-id INTEGER Dashboard ID to add the report to. -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### dashboard-ids Get dashboard IDs containing a bookmark. Returns a list of dashboard IDs that contain the specified bookmark. Uses the `get_bookmark_dashboard_ids` workspace method. Args: ctx: Typer context with global options. bookmark_id: The bookmark identifier. format: Output format (json, jsonl, table, csv, plain). jq_filter: Optional jq filter expression for JSON output. Example: ``` mp reports dashboard-ids 12345 ``` Usage: ``` mp reports dashboard-ids [OPTIONS] BOOKMARK_ID ``` Options: ``` BOOKMARK_ID Bookmark ID to look up. [required] -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### delete Delete a bookmark. Permanently removes the specified bookmark from the project. Args: ctx: Typer context with global options. bookmark_id: The bookmark identifier. Example: ``` mp reports delete 12345 ``` Usage: ``` mp reports delete [OPTIONS] BOOKMARK_ID ``` Options: ``` BOOKMARK_ID Bookmark ID to delete. [required] ``` ##### get Get a single bookmark by ID. Retrieves the full bookmark object from the Mixpanel App API. Args: ctx: Typer context with global options. bookmark_id: The bookmark identifier. format: Output format (json, jsonl, table, csv, plain). jq_filter: Optional jq filter expression for JSON output. Example: ``` mp reports get 12345 mp reports get 12345 --format table ``` Usage: ``` mp reports get [OPTIONS] BOOKMARK_ID ``` Options: ``` BOOKMARK_ID Bookmark ID to retrieve. [required] -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### history Get change history for a bookmark. Returns a paginated list of changes made to the specified bookmark, including who made the change and when. Args: ctx: Typer context with global options. bookmark_id: The bookmark identifier. cursor: Opaque pagination cursor for fetching subsequent pages. page_size: Maximum number of entries per page. format: Output format (json, jsonl, table, csv, plain). jq_filter: Optional jq filter expression for JSON output. Example: ``` mp reports history 12345 mp reports history 12345 --page-size 10 mp reports history 12345 --cursor "abc123" ``` Usage: ``` mp reports history [OPTIONS] BOOKMARK_ID ``` Options: ``` BOOKMARK_ID Bookmark ID to get history for. [required] --cursor TEXT Pagination cursor for next page. --page-size INTEGER Maximum entries per page. -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### linked-dashboards Get dashboard IDs linked to a bookmark. Returns a list of dashboard IDs that reference the specified bookmark via the `bookmark_linked_dashboard_ids` API. Args: ctx: Typer context with global options. bookmark_id: The bookmark identifier. format: Output format (json, jsonl, table, csv, plain). jq_filter: Optional jq filter expression for JSON output. Example: ``` mp reports linked-dashboards 12345 ``` Usage: ``` mp reports linked-dashboards [OPTIONS] BOOKMARK_ID ``` Options: ``` BOOKMARK_ID Bookmark ID to look up. [required] -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### list List bookmarks/reports with optional filters. Retrieves bookmarks from the Mixpanel App API. Optionally filter by report type or specific IDs. Args: ctx: Typer context with global options. bookmark_type: Optional report type filter (e.g., `"funnels"`). ids: Comma-separated bookmark IDs to filter by. format: Output format (json, jsonl, table, csv, plain). jq_filter: Optional jq filter expression for JSON output. Example: ``` mp reports list --type funnels mp reports list --ids 1,2,3 --format table ``` Usage: ``` mp reports list [OPTIONS] ``` Options: ``` -t, --type TEXT Filter by bookmark type (e.g., insights, funnels, flows, retention). --ids TEXT Comma-separated list of bookmark IDs to retrieve. -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### update Update an existing bookmark. Patches the specified bookmark with the provided fields. Only supplied fields are updated; omitted fields remain unchanged. Args: ctx: Typer context with global options. bookmark_id: The bookmark identifier. name: New name for the report. params: Updated report parameters as a JSON string. description: Updated description. format: Output format (json, jsonl, table, csv, plain). jq_filter: Optional jq filter expression for JSON output. Example: ``` mp reports update 12345 --name "Renamed Report" mp reports update 12345 --params '{"events": [{"event": "Login"}]}' ``` Usage: ``` mp reports update [OPTIONS] BOOKMARK_ID ``` Options: ``` BOOKMARK_ID Bookmark ID to update. [required] -n, --name TEXT New name for the report. -p, --params TEXT Updated report parameters as a JSON string. -d, --description TEXT Updated description. -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` #### schemas Manage schema registry definitions. Usage: ``` mp schemas [OPTIONS] COMMAND [ARGS]... ``` ##### create Create a single schema entry. Creates a new schema definition for the specified entity type and name. Args: ctx: Typer context with global options. entity_type: Entity type (event, custom_event, or profile). entity_name: Entity name for the schema. schema_json: Schema definition as a JSON string. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp schemas create [OPTIONS] ``` Options: ``` --entity-type TEXT Entity type (event, custom_event, or profile). [required] --entity-name TEXT Entity name for the schema. [required] --schema-json TEXT Schema definition as a JSON string. [required] -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### create-bulk Bulk create schema entries. Creates multiple schema entries in a single request. Optionally truncates existing schemas before creating. Args: ctx: Typer context with global options. entries: JSON array of schema entries. truncate: Whether to truncate existing schemas before creating. entity_type: Entity type filter for truncation scope. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp schemas create-bulk [OPTIONS] ``` Options: ``` --entries TEXT JSON array of schema entries for bulk creation. [required] --truncate / --no-truncate Truncate existing schemas before creating. [default: no-truncate] --entity-type TEXT Entity type filter for truncation scope. -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### delete Delete schema entries. Deletes schema entries, optionally filtered by entity type and/or entity name. Without filters, deletes all schemas. Args: ctx: Typer context with global options. entity_type: Optional entity type filter for deletion. entity_name: Optional entity name filter for deletion. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp schemas delete [OPTIONS] ``` Options: ``` --entity-type TEXT Entity type filter for deletion. --entity-name TEXT Entity name filter for deletion. -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### list List schema registry entries. Retrieves all schema entries, optionally filtered by entity type. Args: ctx: Typer context with global options. entity_type: Optional entity type filter. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp schemas list [OPTIONS] ``` Options: ``` --entity-type TEXT Filter by entity type (event, custom_event, or profile). -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### update Update a single schema entry (merge semantics). Updates an existing schema definition. Fields provided in the JSON are merged with the existing schema. Args: ctx: Typer context with global options. entity_type: Entity type (event, custom_event, or profile). entity_name: Entity name for the schema. schema_json: Schema updates as a JSON string. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp schemas update [OPTIONS] ``` Options: ``` --entity-type TEXT Entity type (event, custom_event, or profile). [required] --entity-name TEXT Entity name for the schema. [required] --schema-json TEXT Schema updates as a JSON string (merge semantics). [required] -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### update-bulk Bulk update schema entries. Updates multiple schema entries in a single request. Args: ctx: Typer context with global options. entries: JSON array of schema entries for bulk update. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp schemas update-bulk [OPTIONS] ``` Options: ``` --entries TEXT JSON array of schema entries for bulk update. [required] -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` #### session Show / update the active session. Usage: ``` mp session [OPTIONS] COMMAND [ARGS]... ``` Options: ``` --bridge Show bridge file source (Cowork). -f, --format TEXT Output format: text | json [default: text] ``` #### target Manage saved target triples. Usage: ``` mp target [OPTIONS] COMMAND [ARGS]... ``` ##### add Add a new saved target triple. Args: ctx: Typer context. name: Target name (must not already exist). account: Referenced account (must exist). project: Project ID (digit string). workspace: Optional positive workspace ID. Usage: ``` mp target add [OPTIONS] NAME ``` Options: ``` NAME Target name (block key). [required] -a, --account TEXT Referenced account name. [required] -p, --project TEXT Project ID (digit string). [required] -w, --workspace INTEGER Optional workspace ID. ``` ##### list List all configured targets. Args: ctx: Typer context. format: Output format. Usage: ``` mp target list [OPTIONS] ``` Options: ``` -f, --format TEXT Output format: table | json | jsonl. [default: table] ``` ##### remove Remove a target block. Args: ctx: Typer context. name: Target name. yes: Skip confirmation prompt. Usage: ``` mp target remove [OPTIONS] NAME ``` Options: ``` NAME Target to remove. [required] -y, --yes Skip confirmation prompt. ``` ##### show Show a single target's details. Args: ctx: Typer context. name: Target name. format: Output format. Usage: ``` mp target show [OPTIONS] NAME ``` Options: ``` NAME Target name. [required] -f, --format TEXT Output format: table | json. [default: table] ``` ##### use Apply the target β€” write all three axes to `[active]` atomically. Also updates the target account's `default_project` to the target's project, so a fresh `Workspace()` reproduces the same session. Args: ctx: Typer context. name: Target to apply. Usage: ``` mp target use [OPTIONS] NAME ``` Options: ``` NAME Target to apply. [required] ``` #### webhooks Manage project webhooks. Usage: ``` mp webhooks [OPTIONS] COMMAND [ARGS]... ``` ##### create Create a new webhook. Creates a webhook with the specified name and URL. Optional authentication parameters can be provided for secured endpoints. Args: ctx: Typer context with global options. name: Webhook name (required). url: Webhook URL (required). auth_type: Authentication type (e.g. 'basic'). username: Basic auth username. password: Basic auth password. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp webhooks create [OPTIONS] ``` Options: ``` --name TEXT Webhook name. [required] --url TEXT Webhook URL. [required] --auth-type TEXT Auth type (e.g. 'basic'). --username TEXT Basic auth username. --password TEXT Basic auth password. -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### delete Delete a webhook. Permanently deletes a webhook by ID. This action cannot be undone. Args: ctx: Typer context with global options. webhook_id: Webhook UUID string. Usage: ``` mp webhooks delete [OPTIONS] WEBHOOK_ID ``` Options: ``` WEBHOOK_ID Webhook ID (UUID). [required] ``` ##### list List all project webhooks. Retrieves all webhooks configured for the current project. Args: ctx: Typer context with global options. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp webhooks list [OPTIONS] ``` Options: ``` -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### test Test webhook connectivity. Sends a test request to the specified URL and reports the result. Args: ctx: Typer context with global options. url: Webhook URL to test (required). name: Optional webhook name. auth_type: Authentication type. username: Basic auth username. password: Basic auth password. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp webhooks test [OPTIONS] ``` Options: ``` --url TEXT Webhook URL to test. [required] --name TEXT Webhook name. --auth-type TEXT Auth type. --username TEXT Basic auth username. --password TEXT Basic auth password. -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` ##### update Update an existing webhook. Updates webhook fields using PATCH semantics. Only provided fields are modified. Args: ctx: Typer context with global options. webhook_id: Webhook UUID string. name: New webhook name. url: New webhook URL. auth_type: New authentication type. username: New basic auth username. password: New basic auth password. enabled: Enable or disable the webhook. format: Output format. jq_filter: Optional jq filter expression. Usage: ``` mp webhooks update [OPTIONS] WEBHOOK_ID ``` Options: ``` WEBHOOK_ID Webhook ID (UUID). [required] --name TEXT New webhook name. --url TEXT New webhook URL. --auth-type TEXT New auth type. --username TEXT New basic auth username. --password TEXT New basic auth password. --enabled / --no-enabled Enable or disable webhook. -f, --format [json|jsonl|table|csv|plain] Output format. [default: json] --jq TEXT Apply jq filter to JSON output (requires --format json or jsonl). ``` #### workspace Active workspace. Usage: ``` mp workspace [OPTIONS] COMMAND [ARGS]... ``` ##### list List workspaces in the current (or specified) project via /me. Constructs a short-lived :class:`Workspace` against the resolved session, then pulls the workspace list from the cached `/me` response (24h TTL) for `--project` (or the active project when `--project` is omitted). `--refresh` bypasses the cache. Args: ctx: Typer context. project: Project to query (defaults to the active project). refresh: Bypass the local `/me` cache and refetch. format: Output format (`table` / `json` / `jsonl`). Usage: ``` mp workspace list [OPTIONS] ``` Options: ``` -p, --project TEXT Project ID (defaults to active project). --refresh Bypass the local /me cache (FR-047). -f, --format TEXT Output format: table | json | jsonl. [default: table] ``` ##### show Show the currently active workspace. When the active workspace is unset, prints `(workspace will be auto-resolved on first use)`. Args: ctx: Typer context. Usage: ``` mp workspace show [OPTIONS] ``` ##### use Set the active workspace ID. Args: ctx: Typer context. workspace_id: Mixpanel workspace ID (positive int). Usage: ``` mp workspace use [OPTIONS] WORKSPACE_ID ``` Options: ``` WORKSPACE_ID Numeric workspace ID (positive int). [required] ``` Copy markdown # Architecture # Architecture mixpanel_headless follows a layered architecture with clear separation of concerns. Explore on DeepWiki πŸ€– **[Architecture Deep Dive β†’](https://deepwiki.com/mixpanel/mixpanel-headless/5-architecture)** Ask questions about the architecture, trace data flows, or explore component relationships interactively. ## Layer Diagram ``` β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ CLI Layer (Typer) β”‚ β”‚ Argument parsing, output formatting, progress β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Public API Layer β”‚ β”‚ Workspace Β· Account/Session Β· accounts/session/targets β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Service Layer β”‚ β”‚ DiscoveryService, LiveQueryService β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Infrastructure Layer β”‚ β”‚ ConfigManager, MixpanelAPIClient β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` ## Components ### Workspace (Facade) The `Workspace` class is the unified entry point that coordinates all services: - **Session resolution** β€” Three independent axes resolved via `env > param > target > bridge > [active] > default_project`. Single resolver in `_internal/auth/resolver.py`; no silent cross-axis fallback. - **In-session switching** β€” `Workspace.use(account=, project=, workspace=, target=)` returns `self` for chaining and preserves the underlying `httpx.Client` and per-account `/me` cache (O(1) per swap). - **Service Orchestration** β€” Creates and manages service instances - **Entity CRUD** β€” Direct App API access for dashboards, reports, cohorts (workspace-scoped) and feature flags, experiments (project-scoped) - **Data Governance** β€” Schema registry, enforcement, auditing, volume anomalies, event deletion requests, Lexicon definitions, drop filters, custom properties, custom events, and lookup tables - **Resource Management** β€” Context manager support for cleanup ### Services #### DiscoveryService Schema introspection with session-scoped caching: - `list_events()` β€” All event names (cached) - `list_properties(event)` β€” Properties for an event (cached per event) - `list_property_values(property, event)` β€” Sample values (cached) - `list_funnels()` β€” Saved funnels (cached) - `list_cohorts()` β€” Saved cohorts (cached) - `list_top_events()` β€” Today's top events (NOT cached, real-time) #### LiveQueryService Executes live analytics queries against Mixpanel Query API: - Segmentation, funnels, retention, JQL - Event counts, property counts - Activity feed, saved reports, flows, frequency - Numeric aggregations (bucket, sum, average) ### Infrastructure #### ConfigManager TOML-based account management at `~/.mp/config.toml` (single schema; legacy v1/v2 do not load): - Account CRUD over `[accounts.NAME]` blocks - Target CRUD over `[targets.NAME]` blocks - Active-session read/write over the `[active]` block (account + optional workspace) - Atomic writes via temp-file + rename #### MixpanelAPIClient HTTP client with Mixpanel-specific features: - Service account authentication - Regional endpoint routing (US, EU, India) - Automatic rate limit handling with exponential backoff - Streaming JSONL parsing for large exports ### Three-Axis Hierarchy Auth is organized around three independent axes: - **Account** β€” *who* is authenticating. Three first-class types managed through one surface: `service_account` (Basic Auth), `oauth_browser` (PKCE flow, tokens auto-refreshed), `oauth_token` (static bearer for CI/agents). - **Project** β€” *which Mixpanel project* the calls run against. Lives on the active account as `default_project`; can be overridden per call. - **Workspace** β€” *which workspace inside the project*. Optional; lazy-resolves to the project's default workspace on first workspace-scoped call. Persisted (account, project, optional workspace) bundles are called **targets** and act as named cursor positions: `mp target add ecom --account team --project 3018488` then `mp target use ecom`. ## Data Paths ### Live Query Path ``` User Request β†’ Workspace β†’ LiveQueryService β†’ MixpanelAPIClient β†’ Mixpanel API ↓ Typed Result (e.g., SegmentationResult) ``` Best for: - Real-time data needs - One-off analysis - Pre-computed Mixpanel reports ### Streaming Path ``` User Request β†’ Workspace β†’ MixpanelAPIClient β†’ Mixpanel Export API ↓ Iterator[dict] (no storage) ↓ Process each record inline ``` Best for: - ETL pipelines to external systems - One-time processing without storage - Memory-constrained environments - Unix pipeline integration (CLI `--stdout`) ## Key Design Decisions ### Streaming Data Access The API client returns iterators for memory-efficient processing of large datasets without loading everything into memory. ### Immutable Session A `Session` (account + project + optional workspace) is resolved once at `Workspace` construction; `Workspace.use()` swaps in a new `Session` atomically. The `httpx.Client` and per-account `/me` cache are preserved across swaps, so cross-project iteration is O(1) per turn. ### Dependency Injection All services accept their dependencies as constructor arguments. This enables: - Easy testing with mocks - Flexible composition - Clear dependency relationships ## Technology Stack | Component | Technology | Purpose | | ----------------- | ------------ | ----------------------------- | | Language | Python 3.10+ | Type hints, modern syntax | | CLI Framework | Typer | Declarative CLI building | | Output Formatting | Rich | Tables, progress bars, colors | | Validation | Pydantic | Data validation, settings | | HTTP Client | httpx | Async-capable HTTP | ## Package Structure ``` src/mixpanel_headless/ β”œβ”€β”€ __init__.py # Public exports (Workspace, Account, Session, namespaces, exceptions, types) β”œβ”€β”€ workspace.py # Workspace facade with Workspace.use() β”œβ”€β”€ auth_types.py # Public auth surface (Account union, Session, Region, OAuthTokens, BridgeFile, ...) β”œβ”€β”€ accounts.py # mp.accounts namespace (add/list/use/login/test/...) β”œβ”€β”€ session.py # mp.session namespace (show/use) β”œβ”€β”€ targets.py # mp.targets namespace (saved cursors) β”œβ”€β”€ exceptions.py # Exception hierarchy (incl. AccountInUseError, WorkspaceScopeError) β”œβ”€β”€ types.py # Result dataclasses (SegmentationResult, AccountSummary, Target, ...) β”œβ”€β”€ py.typed # PEP 561 marker β”œβ”€β”€ _internal/ β”‚ β”œβ”€β”€ config.py # ConfigManager (single TOML schema) β”‚ β”œβ”€β”€ api_client.py # MixpanelAPIClient β”‚ β”œβ”€β”€ me.py # MeService + per-account MeCache β”‚ β”œβ”€β”€ pagination.py # Cursor-based App API pagination β”‚ β”œβ”€β”€ auth/ β”‚ β”‚ β”œβ”€β”€ account.py # Account variants (ServiceAccount/OAuthBrowserAccount/OAuthTokenAccount) β”‚ β”‚ β”œβ”€β”€ session.py # Session, Project, WorkspaceRef, ActiveSession β”‚ β”‚ β”œβ”€β”€ resolver.py # env > param > target > bridge > [active] resolver β”‚ β”‚ β”œβ”€β”€ token_resolver.py# OnDiskTokenResolver β”‚ β”‚ β”œβ”€β”€ token.py # OAuthTokens, OAuthClientInfo β”‚ β”‚ β”œβ”€β”€ flow.py # OAuth PKCE browser flow β”‚ β”‚ β”œβ”€β”€ bridge.py # Cowork bridge file v2 β”‚ β”‚ β”œβ”€β”€ storage.py # account_dir, ensure_account_dir (atomic 0o600 writes) β”‚ β”‚ β”œβ”€β”€ pkce.py # PKCE challenge generation (RFC 7636) β”‚ β”‚ β”œβ”€β”€ callback_server.py # Local HTTP callback server β”‚ β”‚ └── client_registration.py # Dynamic Client Registration (RFC 7591) β”‚ └── services/ β”‚ β”œβ”€β”€ discovery.py # DiscoveryService β”‚ └── live_query.py # LiveQueryService └── cli/ β”œβ”€β”€ main.py # Typer app + global flags (-a / -p / -w / -t) β”œβ”€β”€ commands/ # account / project / workspace / target / session + query / inspect / ... β”œβ”€β”€ formatters.py # Output formatters └── utils.py # CLI utilities ``` Copy markdown