sdks/python/examples/dashboard_example.ipynb
A comprehensive walkthrough of the dashboard API on the opik.Opik client:
| Step | Topic |
|---|---|
| 1 | Setup |
| 2–3 | Create a MULTI_PROJECT dashboard scoped to a project |
| 4 | Stats-card widgets (snapshot metrics) |
| 5 | Time-series chart widgets |
| 6 | Markdown / notes widget |
| 7 | Update widgets |
| 8 | Inspect and rearrange the grid layout |
| 9 | Add sections and move widgets between sections |
| 10 | Remove widgets |
| 11–12 | Create an EXPERIMENTS dashboard with evaluation widgets |
| 13 | Fetch and list dashboards |
| 14 | Clean up |
Project scope — project_stats_card and project_metrics widgets are project-scoped.
Pass project_name to create_dashboard once; the SDK automatically injects the project
into every project-scoped widget added via add_widget.
Metric-ID namespaces — easy to mix up:
| Widget | Field | Namespace | Example |
|---|---|---|---|
project_stats_card | metric | lowercase-dotted | trace_count, duration.p50 |
project_metrics | metric_type | ALL-CAPS | TRACE_COUNT, DURATION |
%pip install opik --quiet
import copy
import opik
from opik import dashboard
client = opik.Opik()
PROJECT_NAME = "Default Project"
MULTI_PROJECT dashboards support project_stats_card, project_metrics, and text_markdown
widgets. A new dashboard starts with a single Overview section whose id we capture for
adding widgets.
mp_dash = client.create_dashboard(
name="SDK comprehensive demo",
type=dashboard.DashboardType.MULTI_PROJECT,
description="Created from the Python SDK walkthrough",
project_name=PROJECT_NAME,
)
mp_section_id = mp_dash.sections[0].id
print(f"Dashboard id : {mp_dash.id}")
print(f"Type : {mp_dash.type}")
print(f"Scope : {mp_dash.scope}")
print(f"Section id : {mp_section_id}")
The project_name passed to create_dashboard links the dashboard to a project.
The SDK then automatically injects the project into every project-scoped widget
(project_stats_card, project_metrics) when you call add_widget — you do not
need to repeat the project in the widget config.
print(f"Dashboard linked to project: {PROJECT_NAME!r}")
project_stats_card shows a single current-value metric for a project.
The metric field uses the lowercase-dotted namespace — see dashboard.StatsCardMetric
for the full list (trace counts, duration percentiles, token usage, costs, …).
source selects whether the metric is computed over traces or spans.
# Total trace count
sc_trace_id = mp_dash.add_widget(
dashboard.DashboardWidget(
type=dashboard.WidgetType.PROJECT_STATS_CARD,
title="Traces",
config=dashboard.ProjectStatsCardConfig(
source=dashboard.TraceDataType.TRACES,
metric=dashboard.StatsCardMetric.TRACE_COUNT,
),
),
)
# Estimated total cost
sc_cost_id = mp_dash.add_widget(
dashboard.DashboardWidget(
type=dashboard.WidgetType.PROJECT_STATS_CARD,
title="Total cost",
config=dashboard.ProjectStatsCardConfig(
source=dashboard.TraceDataType.TRACES,
metric=dashboard.StatsCardMetric.TOTAL_ESTIMATED_COST_SUM,
),
),
)
# Median latency (p50)
sc_p50_id = mp_dash.add_widget(
dashboard.DashboardWidget(
type=dashboard.WidgetType.PROJECT_STATS_CARD,
title="Latency p50",
config=dashboard.ProjectStatsCardConfig(
source=dashboard.TraceDataType.TRACES,
metric=dashboard.StatsCardMetric.DURATION_P50,
),
),
)
# LLM span count (source=SPANS to query span-level metrics)
sc_llm_id = mp_dash.add_widget(
dashboard.DashboardWidget(
type=dashboard.WidgetType.PROJECT_STATS_CARD,
title="LLM calls",
config=dashboard.ProjectStatsCardConfig(
source=dashboard.TraceDataType.SPANS,
metric=dashboard.StatsCardMetric.LLM_SPAN_COUNT,
),
),
)
print(f"Stats cards added, total widgets: {len(mp_dash.sections[0].widgets)}")
project_metrics renders a time-series for an aggregate metric.
The metric_type field uses the ALL-CAPS namespace — see dashboard.ProjectMetricType.
Breakdowns split the series by a dimension: MODEL, PROVIDER, TAGS, NAME, etc.
Available chart types: LINE (default), BAR, RADAR.
# Line chart: duration over time, broken down by model
chart_duration_id = mp_dash.add_widget(
dashboard.DashboardWidget(
type=dashboard.WidgetType.PROJECT_METRICS,
title="Duration by model",
config=dashboard.ProjectMetricsConfig(
metric_type=dashboard.ProjectMetricType.DURATION,
chart_type=dashboard.ChartType.LINE,
breakdown=dashboard.BreakdownConfig(field=dashboard.BreakdownField.MODEL),
),
),
)
# Bar chart: token usage over time
chart_tokens_id = mp_dash.add_widget(
dashboard.DashboardWidget(
type=dashboard.WidgetType.PROJECT_METRICS,
title="Token usage",
config=dashboard.ProjectMetricsConfig(
metric_type=dashboard.ProjectMetricType.TOKEN_USAGE,
chart_type=dashboard.ChartType.BAR,
),
),
)
# Line chart: trace count broken down by tag
chart_count_id = mp_dash.add_widget(
dashboard.DashboardWidget(
type=dashboard.WidgetType.PROJECT_METRICS,
title="Trace count by tag",
config=dashboard.ProjectMetricsConfig(
metric_type=dashboard.ProjectMetricType.TRACE_COUNT,
chart_type=dashboard.ChartType.LINE,
breakdown=dashboard.BreakdownConfig(field=dashboard.BreakdownField.TAGS),
),
),
)
# Line chart: estimated cost broken down by provider
chart_cost_id = mp_dash.add_widget(
dashboard.DashboardWidget(
type=dashboard.WidgetType.PROJECT_METRICS,
title="Cost by provider",
config=dashboard.ProjectMetricsConfig(
metric_type=dashboard.ProjectMetricType.COST,
chart_type=dashboard.ChartType.LINE,
breakdown=dashboard.BreakdownConfig(
field=dashboard.BreakdownField.PROVIDER
),
),
),
)
print(f"Total widgets: {len(mp_dash.sections[0].widgets)}")
text_markdown renders freeform Markdown — useful for section headers, runbook links,
or context notes. It is valid in both MULTI_PROJECT and EXPERIMENTS dashboards.
Widgets can also be created from a raw dict, which is the forward-compatible path for backend fields not yet modelled in the SDK.
# Typed config
notes_id = mp_dash.add_widget(
dashboard.DashboardWidget(
type=dashboard.WidgetType.TEXT_MARKDOWN,
title="",
config=dashboard.TextMarkdownConfig(
content=(
"## Project overview\n"
"This dashboard tracks **Default Project** metrics.\n\n"
"- Duration p50 / p90\n"
"- Token costs by provider\n"
"- Error rate"
)
),
),
)
# Raw-dict style: forward-compatible with new backend fields
raw_id = mp_dash.add_widget(
{
"type": dashboard.WidgetType.TEXT_MARKDOWN.value,
"title": "Raw dict widget",
"config": {"content": "Built with the `opik` Python SDK."},
},
)
print(f"Total widgets: {len(mp_dash.sections[0].widgets)}")
print(f"Notes id : {notes_id}")
print(f"Raw id : {raw_id}")
update_widget patches only the fields you pass — omitted kwargs are left unchanged.
Config is merged, not replaced, so you can change a single key without restating the
whole config object.
# Change the chart title
mp_dash.update_widget(chart_duration_id, title="Duration by model (ms)")
# Swap the markdown note content (config merge)
mp_dash.update_widget(
notes_id,
config={"content": "## Project overview (updated)\nDashboard refreshed via SDK."},
)
# Add a subtitle to the trace-count stats card
mp_dash.update_widget(sc_trace_id, subtitle="last 7 days")
# Switch the token-usage chart from BAR to LINE
mp_dash.update_widget(
chart_tokens_id,
config=dashboard.ProjectMetricsConfig(
metric_type=dashboard.ProjectMetricType.TOKEN_USAGE,
chart_type=dashboard.ChartType.LINE,
),
)
# Rename the dashboard and update its description
mp_dash.rename("SDK comprehensive demo (v2)")
mp_dash.set_description("Updated via the Python SDK.")
print("Name:", mp_dash.name)
The grid is 6 columns wide with unlimited rows. Each widget has a DashboardLayoutItem
with x (column), y (row), w (width in columns), h (height in rows).
replace_sections swaps the entire sections list in one call — use it to reposition
widgets, resize them, or reorder sections. All other mutators persist immediately after
each individual call.
section = mp_dash.sections[0]
by_id = {w.id: w for w in section.widgets}
print(f"{'title':35s} x y w h")
print("-" * 50)
for li in section.layout:
title = by_id[li.id].title or "(no title)"
print(f"{title:35s} {li.x} {li.y} {li.w} {li.h}")
# Rearrange: full-width notes banner at the top (row 0),
# four stats cards side-by-side below (row 2),
# charts below that (rows 4+).
new_section = copy.deepcopy(section)
stats_ids = [sc_trace_id, sc_cost_id, sc_p50_id, sc_llm_id]
chart_ids = [chart_duration_id, chart_tokens_id, chart_count_id, chart_cost_id]
for li in new_section.layout:
if li.id == notes_id:
# Full-width banner spanning all 6 columns
li.x, li.y, li.w, li.h = 0, 0, 6, 2
elif li.id == raw_id:
# Small note pinned to the top-right
li.x, li.y, li.w, li.h = 4, 2, 2, 2
elif li.id in stats_ids:
col = stats_ids.index(li.id)
li.x, li.y, li.w, li.h = col, 2, 1, 2
elif li.id in chart_ids:
col = chart_ids.index(li.id)
li.x, li.y, li.w, li.h = (col % 3) * 2, 4 + (col // 3) * 4, 2, 4
mp_dash.replace_sections([new_section])
print("Layout after rearrangement:")
section = mp_dash.sections[0]
by_id = {w.id: w for w in section.widgets}
print(f"{'title':35s} x y w h")
print("-" * 50)
for li in section.layout:
title = by_id[li.id].title or "(no title)"
print(f"{title:35s} {li.x} {li.y} {li.w} {li.h}")
add_section appends a new empty section.
To move widgets between sections use replace_sections with the complete new state.
analytics_section_id = mp_dash.add_section("Analytics")
print("Sections:", [s.title for s in mp_dash.sections])
# Move the four chart widgets from Overview into the new Analytics section.
new_sections = [copy.deepcopy(s) for s in mp_dash.sections]
overview, analytics = new_sections
move_ids = {chart_duration_id, chart_tokens_id, chart_count_id, chart_cost_id}
# Extract chart widgets and their layout entries from Overview
moved_widgets = [w for w in overview.widgets if w.id in move_ids]
moved_layout = [li for li in overview.layout if li.id in move_ids]
overview.widgets = [w for w in overview.widgets if w.id not in move_ids]
overview.layout = [li for li in overview.layout if li.id not in move_ids]
# Re-position charts inside Analytics (2-wide, 4-tall, three per row)
for idx, li in enumerate(moved_layout):
li.x, li.y, li.w, li.h = (idx % 3) * 2, (idx // 3) * 4, 2, 4
analytics.widgets.extend(moved_widgets)
analytics.layout.extend(moved_layout)
mp_dash.replace_sections(new_sections)
for s in mp_dash.sections:
print(f" [{s.title}] {len(s.widgets)} widget(s)")
remove_widget removes a widget and its layout entry from whichever section contains it.
Raises DashboardValidationError if the ID is not found.
# Remove the raw-dict markdown widget
mp_dash.remove_widget(raw_id)
total = sum(len(s.widgets) for s in mp_dash.sections)
print(f"Widgets after removal: {total}")
EXPERIMENTS dashboards target evaluation results rather than live traces.
Supported widgets: experiments_feedback_scores, experiment_leaderboard, text_markdown.
exp_dash = client.create_dashboard(
name="SDK experiments demo",
type=dashboard.DashboardType.EXPERIMENTS,
description="Evaluation metrics overview",
)
exp_section_id = exp_dash.sections[0].id
print(f"Experiments dashboard id: {exp_dash.id}")
experiments_feedback_scores plots feedback score distributions across experiments.
experiment_leaderboard shows a ranked table of runs against a chosen metric.
Pass max_experiments_count (1–100) to control how many recent experiments are included.
# Bar chart: feedback scores across the last 10 experiments
fb_bar_id = exp_dash.add_widget(
dashboard.DashboardWidget(
type=dashboard.WidgetType.EXPERIMENTS_FEEDBACK_SCORES,
title="Feedback scores (bar)",
config=dashboard.ExperimentsFeedbackScoresConfig(
chart_type=dashboard.ChartType.BAR,
max_experiments_count=10,
),
),
)
# Radar chart: quality shape across the last 5 experiments
fb_radar_id = exp_dash.add_widget(
dashboard.DashboardWidget(
type=dashboard.WidgetType.EXPERIMENTS_FEEDBACK_SCORES,
title="Feedback scores (radar)",
config=dashboard.ExperimentsFeedbackScoresConfig(
chart_type=dashboard.ChartType.RADAR,
max_experiments_count=5,
),
),
)
# Leaderboard with ranking enabled by a specific feedback-score metric
leaderboard_id = exp_dash.add_widget(
dashboard.DashboardWidget(
type=dashboard.WidgetType.EXPERIMENT_LEADERBOARD,
title="Experiment leaderboard",
config=dashboard.ExperimentLeaderboardConfig(
enable_ranking=True,
ranking_metric="hallucination", # name of the feedback score to rank by
ranking_direction=True, # True = descending (higher score is better)
selected_columns=["dataset_id", "created_at", "duration.p50", "pass_rate"],
max_rows=20,
),
),
)
# Context note
exp_dash.add_widget(
dashboard.DashboardWidget(
type=dashboard.WidgetType.TEXT_MARKDOWN,
title="",
config=dashboard.TextMarkdownConfig(
content="### About\nTracks evaluation runs ranked by the **hallucination** metric."
),
),
)
print(f"Experiments dashboard widgets: {len(exp_dash.sections[0].widgets)}")
get_dashboard retrieves a single dashboard by ID (re-fetches from the backend).
get_dashboards pages through all dashboards with an optional name filter.
# Fetch the multi-project dashboard by id
fetched_mp = client.get_dashboard(mp_dash.id)
print(f"Fetched: {fetched_mp.name!r} ({len(fetched_mp.sections)} section(s))")
# List all dashboards whose name contains "SDK"
found = client.get_dashboards(name="SDK", max_results=20)
print(f"\nDashboards matching 'SDK' ({len(found)} found):")
for d in found:
print(f" {d.id[:8]}… {d.type:15s} {d.name!r}")
mp_dash.delete()
exp_dash.delete()
print("Both dashboards deleted.")