@@ -48,95 +48,117 @@ def create_or_update_dashboard(
4848) -> Dict [str , Any ]:
4949 """Create or update an AI/BI dashboard from JSON content.
5050
51- CRITICAL PRE-REQUISITES (DO NOT SKIP):
52- Before calling this tool, you MUST:
51+ CRITICAL: Before calling this tool, you MUST:
5352 1. Call get_table_details() to get table schemas
54- 2. Call execute_sql() to TEST EVERY dataset query - if any fail, fix them first!
55- 3. Verify query results have expected columns and data types
56-
57- If you skip validation, widgets WILL show "Invalid widget definition" errors!
58-
59- DASHBOARD JSON REQUIREMENTS:
60-
61- Dataset Architecture:
62- - One dataset per domain (orders, customers, products)
63- - Exactly ONE SQL query per dataset (no semicolon-separated queries)
64- - Use fully-qualified table names: catalog.schema.table_name
65- - All widget fieldNames must match dataset column names exactly
66-
67- CRITICAL VERSION REQUIREMENTS:
68- - counter: version 2
69- - table: version 2
70- - filter-multi-select, filter-single-select, filter-date-range-picker: version 2
71- - bar, line, pie: version 3
72- - text: NO spec block (use multilineTextboxSpec directly on widget)
73-
74- CRITICAL FIELD NAME MATCHING:
75- The "name" in query.fields MUST exactly match "fieldName" in encodings!
76- - CORRECT: fields=[{"name": "sum(spend)", "expression": "SUM(`spend`)"}]
77- encodings={"value": {"fieldName": "sum(spend)", ...}}
78- - WRONG: fields=[{"name": "spend", "expression": "SUM(`spend`)"}]
79- encodings={"value": {"fieldName": "sum(spend)", ...}} # ERROR!
80-
81- Widget Field Expressions (ONLY these are allowed):
82- - Aggregates: SUM(`col`), AVG(`col`), COUNT(`col`), COUNT(DISTINCT `col`), MIN(`col`), MAX(`col`)
83- - Date truncation: DATE_TRUNC("DAY", `date`), DATE_TRUNC("WEEK", `date`), DATE_TRUNC("MONTH", `date`)
84- - Simple reference: `column_name`
85- - NO CAST, no complex SQL - put logic in dataset query instead
86-
87- Layout (6-column grid, NO GAPS):
88- - Each row must total width=6 exactly
89- - Counter/KPI: width=2, height=3-4 (NEVER height=2)
90- - Charts: width=3, height=5-6
91- - Tables: width=6, height=5-8
92- - Text headers: width=6, height=1 (use SEPARATE widgets for title and subtitle)
93-
94- Widget Naming:
95- - widget.name: alphanumeric + hyphens + underscores ONLY (no spaces/parentheses/colons)
96- - frame.title: human-readable name (any characters)
97- - widget.queries[0].name: always "main_query"
98-
99- Text Widgets:
100- - Do NOT use a spec block - use multilineTextboxSpec directly on widget
101- - Multiple items in lines[] are CONCATENATED, not separate lines
102- - Use separate text widgets for title and subtitle at different y positions
103-
104- Counter Widgets (version 2):
105- Pattern 1 - Pre-aggregated (1 row, no filters):
106- - Dataset returns exactly 1 row
107- - Use "disaggregated": true and simple field reference
108- Pattern 2 - Aggregating (multi-row, supports filters):
109- - Dataset returns multiple rows (grouped by filter dimension)
110- - Use "disaggregated": false and aggregation expression
111- - Field name must match: {"name": "sum(spend)", "expression": "SUM(`spend`)"}
112- - Percent values must be 0-1 (not 0-100)
113-
114- Table Widgets (version 2):
115- - Column objects only need fieldName and displayName - no other properties!
116- - Use "disaggregated": true for raw rows
117-
118- Charts - line/bar/pie (version 3):
119- - Use "disaggregated": true with pre-aggregated data
120- - scale.type: "temporal" (dates), "quantitative" (numbers), "categorical" (strings)
121- - Limit color/grouping dimensions to 3-8 distinct values
122-
123- SQL Patterns (Spark SQL):
124- - Date math: date_sub(current_date(), N), add_months(current_date(), -N)
125- - AVOID INTERVAL syntax - use functions instead
126-
127- Filters (CRITICAL - Global vs Page-Level):
128- - Valid filter widgetTypes: "filter-multi-select", "filter-single-select", "filter-date-range-picker"
129- - Filter widgets use spec.version: 2 (NOT 3 like charts)
130- - Filter encodings require "queryName" to bind to dataset queries
131- - Use "disaggregated": false for filter queries
132- - DO NOT use widgetType: "filter" - this is INVALID and will cause errors
133- - DO NOT use associative_filter_predicate_group - causes SQL errors
134- - ALWAYS include "frame": {"showTitle": true, "title": "..."} for filter widgets
135-
136- Global Filters vs Page-Level Filters:
137- - GLOBAL: Place on page with "pageType": "PAGE_TYPE_GLOBAL_FILTERS" - affects ALL pages
138- - PAGE-LEVEL: Place on regular "PAGE_TYPE_CANVAS" page - affects ONLY that page
139- - A filter only affects datasets containing the filter field column
53+ 2. Call execute_sql() to TEST EVERY dataset query
54+ If you skip validation, widgets WILL show errors!
55+
56+ WIDGET STRUCTURE (CRITICAL - follow this exactly):
57+ Each widget in a page layout has `queries` as a TOP-LEVEL SIBLING of `spec`.
58+ Do NOT put queries inside spec. Do NOT use `named_queries`.
59+
60+ Correct counter widget:
61+ {
62+ "widget": {
63+ "name": "total-trips",
64+ "queries": [
65+ {
66+ "name": "main_query",
67+ "query": {
68+ "datasetName": "summary",
69+ "fields": [{"name": "sum(trips)", "expression": "SUM(`trips`)"}],
70+ "disaggregated": false
71+ }
72+ }
73+ ],
74+ "spec": {
75+ "version": 2,
76+ "widgetType": "counter",
77+ "encodings": {
78+ "value": {"fieldName": "sum(trips)", "displayName": "Total Trips"}
79+ },
80+ "frame": {"showTitle": true, "title": "Total Trips"}
81+ }
82+ },
83+ "position": {"x": 0, "y": 0, "width": 2, "height": 3}
84+ }
85+
86+ Correct bar chart widget:
87+ {
88+ "widget": {
89+ "name": "trips-by-zip",
90+ "queries": [
91+ {
92+ "name": "main_query",
93+ "query": {
94+ "datasetName": "by_zip",
95+ "fields": [
96+ {"name": "pickup_zip", "expression": "`pickup_zip`"},
97+ {"name": "trip_count", "expression": "`trip_count`"}
98+ ],
99+ "disaggregated": true
100+ }
101+ }
102+ ],
103+ "spec": {
104+ "version": 3,
105+ "widgetType": "bar",
106+ "encodings": {
107+ "x": {"fieldName": "pickup_zip", "scale": {"type": "categorical"}, "displayName": "ZIP"},
108+ "y": {"fieldName": "trip_count", "scale": {"type": "quantitative"}, "displayName": "Trips"}
109+ },
110+ "frame": {"showTitle": true, "title": "Trips by ZIP"}
111+ }
112+ },
113+ "position": {"x": 0, "y": 3, "width": 6, "height": 5}
114+ }
115+
116+ Correct filter widget:
117+ {
118+ "widget": {
119+ "name": "filter-region",
120+ "queries": [
121+ {
122+ "name": "main_query",
123+ "query": {
124+ "datasetName": "sales",
125+ "fields": [{"name": "region", "expression": "`region`"}],
126+ "disaggregated": false
127+ }
128+ }
129+ ],
130+ "spec": {
131+ "version": 2,
132+ "widgetType": "filter-multi-select",
133+ "encodings": {
134+ "fields": [{"fieldName": "region", "queryName": "main_query", "displayName": "Region"}]
135+ },
136+ "frame": {"showTitle": true, "title": "Region"}
137+ }
138+ },
139+ "position": {"x": 0, "y": 0, "width": 2, "height": 2}
140+ }
141+
142+ Text widget (NO spec block):
143+ {
144+ "widget": {
145+ "name": "title",
146+ "textbox_spec": "## Dashboard Title"
147+ },
148+ "position": {"x": 0, "y": 0, "width": 6, "height": 1}
149+ }
150+
151+ KEY RULES:
152+ - queries[].query.datasetName (camelCase, not dataSetName)
153+ - queries[].query.fields[].name MUST exactly match encodings fieldName
154+ - Versions: counter=2, table=2, filters=2, bar/line/pie=3
155+ - Layout: 6-column grid, each row must sum to width=6
156+ - Filter widgetType must be "filter-multi-select", "filter-single-select",
157+ or "filter-date-range-picker" (NOT "filter")
158+ - Global filters: page with "pageType": "PAGE_TYPE_GLOBAL_FILTERS"
159+ - Page-level filters: on regular "PAGE_TYPE_CANVAS" page
160+
161+ See the databricks-aibi-dashboards skill for full reference.
140162
141163 Args:
142164 display_name: Dashboard display name
@@ -146,14 +168,7 @@ def create_or_update_dashboard(
146168 publish: Whether to publish after creation (default: True)
147169
148170 Returns:
149- Dictionary with:
150- - success: Whether operation succeeded
151- - status: 'created' or 'updated'
152- - dashboard_id: Dashboard ID
153- - path: Full workspace path
154- - url: Dashboard URL
155- - published: Whether dashboard was published
156- - error: Error message if failed
171+ Dictionary with success, status, dashboard_id, path, url, published, error.
157172 """
158173 # MCP deserializes JSON params, so serialized_dashboard may arrive as a dict
159174 if isinstance (serialized_dashboard , dict ):
0 commit comments