Create a dashboard with an agent
Create dashboards through a connected agent. Use the web app to view them. You need the Explorer role or higher to create or edit a Dashboard definition. Dashboard queries use the Workspace Connector and follow the same Workspace role and warehouse rules as Explore. The standalone analysis used below follows its own access requirements.
Start from a verified analysis
Section titled “Start from a verified analysis”- Run the metric query or SQL query you want to reuse. Confirm its connector, inputs, and result columns. Semantic queries use shared metrics; SQL queries use the Connector’s warehouse dialect.
- Tell the agent which workspace to use and what the dashboard should show. For example: “Create a Revenue overview dashboard with a table of the revenue metric. Use the connector and metric from the analysis we just checked.”
- Have the agent discover the Workspace, read
workspace, and followavailableOperationsto list dashboards and avoid creating an unwanted duplicate. Send the advertisedDASHBOARD_DEFINITIONcreate entry tomonomer_describebefore building the definition. - Open Dashboards in that workspace and select the new dashboard. Check its title and query result against the original analysis.
A complete table example
Section titled “A complete table example”After describing the advertised create entry, search for and read the referenced metric and model to confirm their meaning and fields. With real workspace and connector IDs and an existing revenue metric, call monomer_create with this example. Replace both illustrative UUIDs. metric.revenue is the result column for the selected metric.
monomer_create input
{ "workspaceId": "0198ef28-2000-7000-8000-000000000001", "path": "workspace/dashboards", "input": { "kind": "DASHBOARD_DEFINITION", "definition": { "schemaVersion": 1, "meta": {"title": "Revenue overview"}, "parameters": {}, "variables": {}, "queries": { "revenue_total": { "type": "SEMANTIC", "connectorId": "0198ef28-2000-7000-8000-000000000002", "query": {"schemaVersion": 4, "metrics": ["revenue"], "groupBys": [], "metricTimeRange": null, "filters": [], "orderBy": [], "rowLimit": 100} } }, "widgets": {"revenue_table": {"type": "TABLE", "queryName": "revenue_total", "columns": [{"field": "metric.revenue", "header": "Revenue"}]}}, "layout": {"sections": [{"id": "main", "height": {"mode": "SCALED", "scale": 1}, "root": {"type": "WIDGET", "id": "revenue_node", "widgetId": "revenue_table"}}]} } }}The TABLE has no module, so the host formats each cell: with the metric’s saved display format, or else with the digits its values need and without warehouse padding, as in Metric display in widgets. Sorting and copying keep the exact value. To round to fixed decimal places or show a currency, set the metric’s display format instead of writing code. Add a TABLE module only for presentation the host doesn’t provide, such as colors, labels, or comparisons; see Format dashboard table cells.
This example shows an unfiltered total. Add a reporting period or grouping only after verifying those query inputs. Read the returned dashboard path and inspect the saved definition. A saved dashboard does not prove its queries succeeded: open a Session and follow its QueryRuns to results.
Use the Dashboard definition schema when adding chart, text, or input widgets. For CHART module.source, follow Write a dashboard chart source for defineChart, result rows, and minimal line and big-number examples. For TABLE module.source, follow Format dashboard table cells for defineTable, the displayText input that keeps the host’s formatting, Intl and dates, and examples for heatmaps, status labels, changes, and dates. To show a metric where two sets of dimension values cross, such as region by month, give the TABLE pivot instead of columns; follow Build a dashboard pivot table for the layout rules and totals. For changes to an existing dashboard, follow Review and apply dashboard changes.
Use a SQL query
Section titled “Use a SQL query”Every named query has an explicit type: SEMANTIC or SQL. Both can appear in one dashboard, and widgets continue to refer to their query names. A SQL query declares its Connector, template, bindings, result column names, and row limit. After verifying the tables and columns on a PostgreSQL Connector, a query entry can be:
{ "type": "SQL", "connectorId": "0198ef28-2000-7000-8000-000000000002", "sql": "SELECT customer_id AS customer, amount AS revenue FROM analytics.orders WHERE country = {{country}} AND order_date BETWEEN {{from}} AND {{to}} ORDER BY amount DESC LIMIT 100", "bindings": { "country": {"kind": "VARIABLE", "name": "selectedCountry"}, "from": {"kind": "PARAMETER", "name": "period", "endpoint": "start"}, "to": {"kind": "PARAMETER", "name": "period", "endpoint": "end"} }, "columns": ["customer", "revenue"], "rowLimit": 100}Declare selectedCountry as a TEXT variable and period as a DATE_RANGE parameter in the dashboard. Bind the complete parameter map when opening or starting a draft. A TABLE referencing this query can use {"field":"revenue","header":"Revenue"}; a chart reads row["revenue"]. SQL result keys use the declared result names. SQL columns have no metric display format, so the host shows the digits their values need. To round one to fixed decimal places, give the TABLE a module whose cell passes the original decimal string to Intl.NumberFormat, as described in Format dashboard table cells.
Bind values through templates
Section titled “Bind values through templates”Place a {{alias}} wherever the warehouse expects a value. Alias names start with an ASCII letter or underscore, followed by ASCII letters, digits, or underscores; spaces and tabs may surround the alias inside the braces. Every alias must be declared in bindings and used at least once. Repeated aliases are allowed and send the value again in appearance order. Binding map order has no effect.
Monomer replaces each matching template with ? and sends its typed value separately. It does no other SQL parsing or rewriting. Templates are reserved everywhere in the source, including strings and comments: do not put them inside quotes or comments, and do not use them for table names, column names, operators, or SQL fragments. Write country = {{country}}, rather than country = '{{country}}'. An input such as x' OR '1'='1 stays a bound text value.
Use templates for all input values; a raw ? does not have a Dashboard binding. On PostgreSQL, the JDBC spelling of the JSONB ? operator is ??. Do not pass reader-controlled values into warehouse dynamic-SQL functions or procedures: the warehouse can reinterpret those values as SQL, outside the value-binding guarantee.
Bindings support TEXT, NUMBER, BOOLEAN, DATE, TIMESTAMP, and range endpoints. SET values are excluded. A range reference requires endpoint: "start" or "end"; scalar references omit it. Nullable values are sent as nulls with their declared type, and a null range supplies a typed null for either endpoint. SQL decides how null behaves: use a suitable warehouse expression if null should mean “All regions.”
NUMBER uses a canonical decimal string within precision 38 and scale 18, such as "123.45"; values that an engine cannot represent exactly fail rather than round. BigQuery uses BIGNUMERIC when NUMERIC cannot hold the value exactly. DATE is a calendar date. TIMESTAMP is a UTC instant with at most microsecond precision.
Databricks uses the SQL Statement Execution API with typed, named parameters. TIMESTAMP inputs preserve the UTC instant, including microseconds, and NUMBER inputs such as "0.0001" are supported within the same precision 38 and scale 18 limits. Monomer renders the parameter markers for your connector; write {{alias}} outside quotes and keep values in bindings. Quotes and comments are not parsed, so successful validation does not prove that every binding is used. Inputs that cannot be represented exactly fail with QUERY_BIND_UNSUPPORTED; Databricks rejects all BYTES inputs, including typed nulls, which Dashboard inputs do not produce. Page readers receive QUERY_FAILED without SQL or Connector details.
Verify the result contract
Section titled “Verify the result contract”columns contains 1–1,000 unique names in result order. Names must be NFC-normalized, have no control characters or surrounding whitespace, and match the final result names exactly, including case. Use explicit aliases and the warehouse’s quoting rules where needed. A mismatch fails the run before publishing results.
rowLimit is required, from 1 to 100,000. Exceeding it fails the run; results are not truncated. Monomer does not add a LIMIT clause. The SQL source is limited to 256 KiB in UTF-8 bytes. Before each new SQL run, the warehouse’s native non-executing check rejects common accidents; this check is not a read-only guarantee. A reused successful result does not run the check or SQL again.
Open the dashboard or shared draft and inspect each QueryRun and result before applying. CHART and TABLE widgets reading any SQL query, including a TABLE’s auxiliary query, show SQL. For displayed results, the badge uses the query type recorded for those runs, including results kept during a refresh. Before results are ready, it uses the query types in the dashboard definition. Badge changes do not resize the widget: its header and minimum height follow the definition and available host actions. When a badge is needed on a box without a header, it stays visible over the box without taking up layout space. Explorers can select Show SQL to inspect the stored definition template; mixed widgets also offer Show metric definitions for their Semantic queries. Page readers see the badge without SQL text or Connector details.
Use a dedicated read-only Connector account and restrict procedures, pass-through, and remote/external functions. SQL can reach any source its warehouse identity permits, including tables not selected in Catalog. Sharing in Pages lets readers change inputs and execute the saved SQL; see Share Pages publicly and Workspace roles.
Choose the section height
Section titled “Choose the section height”Every section declares height. Use "height": {"mode": "CONTENT"} for TEXT and INPUT widgets, including a ROW or COLUMN combining them. The section follows the rendered content and standard padding, including Markdown wrapping and images. The example below is a single text section.
monomer_create input
{ "workspaceId": "0198ef28-2000-7000-8000-000000000001", "path": "workspace/dashboards", "input": { "kind": "DASHBOARD_DEFINITION", "definition": { "schemaVersion": 1, "meta": {"title": "Summary"}, "parameters": {}, "variables": {}, "queries": {}, "widgets": {"summary_text": {"type": "TEXT", "content": "## Overview\nA short explanation."}}, "layout": {"sections": [{ "id": "summary", "height": {"mode": "CONTENT"}, "root": {"type": "WIDGET", "id": "summary_node", "widgetId": "summary_text"} }]} } }}Use "height": {"mode": "SCALED", "scale": 1.5} when the section contains a CHART, TABLE, or a SPACER directly inside a COLUMN. Scale ranges from 0.3 to 10 in 0.1 increments and sets the wide-screen minimum height to scale × 300 CSS pixels. Content and widget minimums can make the section taller. A section containing only TEXT and INPUT must use CONTENT; adding a CHART or TABLE requires SCALED in the same draft edit. CONTENT does not accept scale, and the old heightScale field is not accepted.
Combine widgets with STACK
Section titled “Combine widgets with STACK”A STACK declares direction: "ROW" or "COLUMN" and at least two children, each containing a node. Nodes are WIDGET, STACK, or SPACER. Give each section and node a unique ID, and place every declared widget exactly once. Adjacent SPACER children are rejected. Nested STACKs preserve their groups, including when parent and child have the same direction.
- In a ROW, every child has a positive integer
weight, which divides the available width. - In a COLUMN, a child containing a CHART, TABLE, or COLUMN SPACER needs
weight. A child containing only TEXT or INPUT uses its content height and must omitweight. A SPACER needs height only when its direct parent is a COLUMN. - For each STACK, two or more declared weights must have greatest common divisor 1. A single declared weight must be 1. A COLUMN with no weights needs no weight normalization.
A COLUMN can put a content-sized introduction above a chart. This layout fragment assumes intro is TEXT and trend is CHART:
{ "id": "trend_section", "height": {"mode": "SCALED", "scale": 1.5}, "root": { "type": "STACK", "id": "trend_column", "direction": "COLUMN", "children": [ {"node": {"type": "WIDGET", "id": "intro_node", "widgetId": "intro"}}, {"weight": 1, "node": {"type": "WIDGET", "id": "trend_node", "widgetId": "trend"}} ] }}The introduction takes its content height, and the chart receives the remaining height subject to its minimum. Add "separation": "DIVIDER" to share one surface with dividers between direct children. Omit separation for the default GAP between independent surfaces.
CHART and TABLE boxes keep the same size while loading, showing errors, or displaying results. A ROW stacks its children vertically when their minimum widths do not fit. Narrow dashboard containers also use stacked layouts with fixed chart and table heights; tables scroll inside their boxes. The saved definition does not need separate desktop or mobile layouts.
Place inputs where readers use them
Section titled “Place inputs where readers use them”Put related filters in a CONTENT ROW above the results, or place a content-sized INPUT above a chart in a COLUMN. In a ROW beside a chart, an INPUT remains top-aligned at its content height; it does not stretch to fill the chart’s height. SET and RANGE inputs need more width than scalar inputs, so allow room for their controls or let the ROW stack on narrow containers.
Keep long explanations in their own CONTENT section. A long TEXT beside a chart in a ROW makes that chart grow to the same height. Changing a TEXT widget to CHART also requires adding its COLUMN weight, if applicable, and changing a CONTENT section to SCALED in the same draft edit.
Label widgets and empty choices
Section titled “Label widgets and empty choices”CHART and TABLE widgets accept an optional title, shown as the widget heading. A choice control (one with options) on a nullable parameter or input variable accepts an optional emptyLabel for the empty choice, such as "All regions"; the web app shows Any when it is absent. Both are single-line text of 1 to 120 characters without leading or trailing spaces. emptyLabel is rejected on free-input controls and on non-nullable values.
This monomer_create example assumes the revenue metric can query a TEXT dimension named region on the orders model. Choosing All regions sets the variable to null, and "emptyBehavior": "OMIT" then drops the filter.
monomer_create input
{ "workspaceId": "0198ef28-2000-7000-8000-000000000001", "path": "workspace/dashboards", "input": { "kind": "DASHBOARD_DEFINITION", "definition": { "schemaVersion": 1, "meta": {"title": "Revenue by region"}, "parameters": {}, "variables": {"region": {"type": "TEXT", "nullable": true, "label": "Region", "initial": null}}, "queries": { "regional_revenue": { "type": "SEMANTIC", "connectorId": "0198ef28-2000-7000-8000-000000000002", "query": { "schemaVersion": 4, "metrics": ["revenue"], "groupBys": [], "metricTimeRange": null, "filters": [{"kind": "TEXT", "dimension": {"semanticModelName": "orders", "name": "region"}, "operator": "EQUALS", "value": {"kind": "VARIABLE", "name": "region"}, "emptyBehavior": "OMIT"}], "orderBy": [], "rowLimit": 100 } } }, "widgets": { "region_input": {"type": "INPUT", "variableName": "region", "control": {"options": [{"value": "north", "label": "North"}, {"value": "south", "label": "South"}], "emptyLabel": "All regions"}}, "revenue_table": {"type": "TABLE", "title": "Revenue for the selected region", "queryName": "regional_revenue", "columns": [{"field": "metric.revenue", "header": "Revenue"}]} }, "layout": {"sections": [ {"id": "filters", "height": {"mode": "CONTENT"}, "root": {"type": "WIDGET", "id": "region_node", "widgetId": "region_input"}}, {"id": "results", "height": {"mode": "SCALED", "scale": 1}, "root": {"type": "WIDGET", "id": "revenue_node", "widgetId": "revenue_table"}} ]} } }}Connect a customer input to a query
Section titled “Connect a customer input to a query”A customer input needs three connections: a typed variable, an INPUT widget naming that variable, and a query filter referencing it. Declaring a variable or adding an input alone does not filter a query.
This complete monomer_create example assumes the revenue metric can query an existing TEXT dimension named customer_id on the orders model. Replace the illustrative IDs and initial customer value after verifying a typed customer query.
monomer_create input
{ "workspaceId": "0198ef28-2000-7000-8000-000000000001", "path": "workspace/dashboards", "input": { "kind": "DASHBOARD_DEFINITION", "definition": { "schemaVersion": 1, "meta": {"title": "Revenue by customer"}, "parameters": {}, "variables": {"customer_id": {"type": "TEXT", "nullable": false, "label": "Customer ID", "initial": "customer_123"}}, "queries": { "customer_revenue": { "type": "SEMANTIC", "connectorId": "0198ef28-2000-7000-8000-000000000002", "query": { "schemaVersion": 4, "metrics": ["revenue"], "groupBys": [], "metricTimeRange": null, "filters": [{"kind": "TEXT", "dimension": {"semanticModelName": "orders", "name": "customer_id"}, "operator": "EQUALS", "value": {"kind": "VARIABLE", "name": "customer_id"}}], "orderBy": [], "rowLimit": 100 } } }, "widgets": { "customer_input": {"type": "INPUT", "variableName": "customer_id", "control": {"placeholder": "Enter customer ID"}}, "revenue_table": {"type": "TABLE", "queryName": "customer_revenue", "columns": [{"field": "metric.revenue", "header": "Revenue"}]} }, "layout": {"sections": [ {"id": "filters", "height": {"mode": "CONTENT"}, "root": {"type": "WIDGET", "id": "customer_node", "widgetId": "customer_input"}}, {"id": "results", "height": {"mode": "SCALED", "scale": 1}, "root": {"type": "WIDGET", "id": "revenue_node", "widgetId": "revenue_table"}} ]} } }}Dashboard query filters use value for a literal or a variable/parameter reference. A standalone Semantic query uses values; Dashboard resolves the reference into that query input. Match the variable type and filter kind to the model dimension. For a NUMBER customer ID, both must be NUMBER and the initial value must be a canonical decimal string such as "123". A time dimension defined at day or coarser grain takes a DATE filter and a DATE variable; one defined at hour or finer grain takes TIMESTAMP. Monomer reads both in the Workspace’s Reporting time zone.
Open the dashboard, change Customer ID to another known customer, and compare the result with a standalone query for that customer. The input value belongs to the current viewer’s screen. To update an existing dashboard, make the same connected changes in its shared draft and verify the affected query before applying.
To run the dashboard with time-based inputs, continue with Schedule a dashboard.
Metric display in widgets
Section titled “Metric display in widgets”A host-rendered TABLE uses the metric display format saved with its query run. Numeric columns without a metric format use the same exact minimal digits as Explore, so 10430427.950000000 reads as 10,430,427.95. Raw sorting and copying keep the original values. A TABLE module that returns undefined keeps the host display; a returned CellView.text supplies the displayed text. Each cell’s displayText input is exactly the host’s text for that cell, so a module can return { text: displayText, background } to change only the style. See Format dashboard table cells.
Chart and TABLE frames include numberFormat on every result column. It contains standard Intl.NumberFormat options derived from the saved metric format, or null for Default and SQL columns. Chart and TABLE code choose whether to apply it. Pass the original decimal string to format, for example:
const column = queries.sales.columns.find(column => column.name === "metric.revenue");const formatter = new Intl.NumberFormat(locale, column?.numberFormat ?? {});const label = formatter.format(row["metric.revenue"]);Pages showing a dashboard use the same saved result formats, including publicly shared Pages. Changing a metric format does not rewrite an existing dashboard query run.