Skip to content

Query Widgets

Query Widgets let you write SQL for a dashboard widget. Use them when a normal widget calculation is too limited, or when you want to build a widget on top of a reusable Query Dataset.

A Query Widget has one source: a Dataset or a Query Dataset. Inside the Widget Query, that source is always named source.

The full YAML field list, including every display option, is in Query Widget YAML.

Use a Query Widget when you need:

  • A table, metric, bar chart, line chart, pie chart, gauge, or funnel from SQL
  • Custom grouping or filtering that is easier to express in SQL
  • A dashboard widget over a Query Dataset
  • Filter Variables passed into SQL as safe parameters
  • A small final query on top of a reusable source

Use a normal widget when the built-in calculation editor already covers the metric. Normal widgets are still easier for common counts, sums, averages, grouped charts, and simple table views.

Query Widgets and Query Datasets work together, but they solve different problems.

Use this For this
Query Dataset Joins, CTEs, subqueries, window functions, and reusable SQL-defined data
Query Widget The final dashboard query, display settings, and Filter Variable binding
Normal widget Standard chart, table, or KPI calculations without writing SQL
Modified Dataset No-SQL data cleanup, filters, calculated columns, and aggregations
Joined Dataset No-SQL joins and unions across datasets

If your SQL needs a join, put that join in a Query Dataset first. Then create a Query Widget that selects from the Query Dataset.

A Query Widget source cannot be a Modified Dataset or Joined Dataset. Shape that data in a Query Dataset, then point the widget at the Query Dataset.

  1. Open a dashboard.
  2. Click the Edit icon.
  3. Click + Widget.
  4. Choose New Query Widget.
  5. Select or paste a source in the YAML definition.
  6. Write the Widget Query.
  7. Configure the display block.
  8. Click Preview to test it.
  9. Click Save & Close.

The preview runs immediately in the editor. The saved dashboard result updates through the normal widget refresh worker.

Keep query as the last top-level key.

schemaVersion: 1
source:
type: dataset
source: 46b0bbe8-619a-4b23-bc44-f11842775640
display:
type: table
timezone: America/Chicago
query: |-
select *
from source
limit 100
Field Required What it does
schemaVersion yes Use 1 for the current Query Widget format
source.type yes dataset or queryDataset
source.source yes Source UUID copied from Browse Sources
display yes Controls how the result is rendered
timezone no IANA timezone. Datetime columns are localized before SQL
parameters no Filter Variable bindings (name + filterVariable UUID)
drilldownQuery no Optional SQL for on-demand detailed rows
query yes Read-only SQL that selects from the relation named source

See Query Widget YAML for the complete field reference.

A Widget Query can:

  • Select from source
  • Filter rows
  • Group and aggregate
  • Use case expressions
  • Sort and limit rows
  • Use declared Query Parameters such as $board_values
  • Use dashboard date helpers such as dashboard_date_filter(column)

A Widget Query cannot:

  • Join another source
  • Use CTEs
  • Use subqueries
  • Use unions
  • Select from a source name or UUID directly
  • Change data
  • Read or write files

These rules keep saved widgets predictable. If you need joins or CTEs, create a Query Dataset first.

Table displays (display.type: table) return at most 1,000 rows from the main Widget Query in preview and on the dashboard. If the query would return more, the result is truncated and the widget shows a limited-rows indicator. Put larger detail sets in drilldownQuery — drilldown is not limited to 1,000 rows.

Non-table displays use a higher general result cap. Prefer filters, aggregates, or an explicit LIMIT in table Widget Queries so the capped page is the set you intend viewers to see.

Most complex Query Widgets should point at a Query Dataset:

schemaVersion: 1
source:
type: queryDataset
source: 5a242d39-195c-41da-a99b-93b402a4325b
display:
type: table
timezone: America/Chicago
query: |-
select *
from source
order by last_email_age_minutes desc
limit 100

Even though the source is a Query Dataset, the Widget Query still selects from source. Do not select from the Query Dataset UUID.

Query Widgets do not automatically inherit every dashboard filter. Add Query Parameters for the Filter Variables you want to use.

parameters:
- name: board_values
filterVariable: 32b69796-14b3-48d1-a9a7-d196b91752c1

Then reference the parameter in SQL with $name:

select count(*) as ticket_count
from source
where board_name in $board_values

Use in for multi-select Filter Variables. Use = for single-select Filter Variables.

Parameters are sent to the query safely. They are not pasted into the SQL as text.

Datetime columns from the source are converted into the widget timezone before the Widget Query runs. Use an IANA name such as America/Chicago.

To use the active dashboard date filter, call dashboard_date_filter() with the date or datetime column you want to filter:

select
date_trunc('day', created_at) as created_day,
count(*) as ticket_count
from source
where dashboard_date_filter(created_at)
group by created_day
order by created_day

If no dashboard date filter is active, the helper allows all rows.

When the widget should fall back to a date range instead of matching all rows, use dashboard_date_filter_or_default():

select
date_trunc('day', created_at) as created_day,
count(*) as ticket_count
from source
where dashboard_date_filter_or_default(
created_at,
runtime_start_date('this month'),
runtime_end_date('this month')
)
group by created_day
order by created_day

For point-in-time calculations, use dashboard_start_date(default_date) or dashboard_end_date(default_date). Each returns the active dashboard boundary when a filter is set and the supplied default otherwise:

select account_name, sum(amount) as balance
from source
where posted_on <= dashboard_end_date(runtime_end_date('today'))
group by account_name

Choose based on what the widget measures, not its dataset. Activity and flow widgets use the selected date range. Point-in-time widgets use the selected end date and can include relevant history before the selected start date. On a dashboard, the widget identifies its interpretation as either the selected range or As of <end date>.

display is required. display.type must be one of bar, line, pie, metric, table, gauge, or funnel.

display:
type: metric
value: ticket_count
rounding: 0
query: |-
select count(*) as ticket_count
from source

value is the numeric result column from the first row.

display:
type: table
columns:
- ticket_number
- ticket_subject
- board_name
- last_email_age_minutes
columnFormats:
last_email_age_minutes:
rounding: 0
columnLinks:
ticket_number: "https://psa.example.com/tickets/{{ ticket_number }}"
query: |-
select
ticket_number,
ticket_subject,
board_name,
last_email_age_minutes
from source
order by last_email_age_minutes desc
limit 100

Use columnLinks to turn matching table or drilldown cells into external http(s) links. Templates support {{ column_name }} placeholders filled from the row and {{ conn.variable_name }} placeholders filled from the source integration config. See Query Widget YAML for auto-fill and authoring rules. Unsafe schemes fall back to plain text.

y is a list of numeric result column names, or a single column name. xLabel and yLabel are axis titles on display.

display:
type: bar
x: board_name
y:
- ticket_count
xLabel: Board
yLabel: Tickets
dataLabels: true
query: |-
select
board_name,
count(*) as ticket_count
from source
group by board_name
order by ticket_count desc

For long-form data, set seriesBy to the category column that splits series. seriesBy requires exactly one y column. Aggregate the Widget Query so each (x, seriesBy) pair is unique.

display:
type: bar
x: facility
y:
- open_shift_pct
seriesBy: shift_type
yLabel: Uncovered shift rate

Wide data uses multiple y columns and omits seriesBy:

display:
type: bar
x: board_name
y:
- open_count
- closed_count
yLabel: Tickets
display:
type: line
x: created_day
y:
- ticket_count
areaFill: true
query: |-
select
date_trunc('day', created_at) as created_day,
count(*) as ticket_count
from source
where dashboard_date_filter(created_at)
group by created_day
order by created_day
display:
type: pie
label: status_name
value: ticket_count
donut: true
query: |-
select
status_name,
count(*) as ticket_count
from source
group by status_name

Pie and funnel charts use label and value, not x and y.

See Query Widget YAML for gauge, funnel, metric comparisons, shared number formatting, and table column options.

Some source columns include dots, such as board.name or company.identifier. In SQL, a dot usually means a table or struct field. If the column name itself contains a dot, wrap it in double quotes:

select count(*) as ticket_count
from source
where "board.name" = 'Service Board'

Better yet, alias it to a simple name in a Query Dataset:

select tickets."board.name" as board_name
from tickets

Then the Query Widget can use board_name.

Use drilldownQuery when you want a Query Widget to provide detailed rows for a modal or export. It follows the same source and parameter rules as the main Widget Query.

drilldownQuery: |-
select
ticket_number,
ticket_subject,
board_name,
last_email_age_minutes
from source
where board_name in $board_values
order by last_email_age_minutes desc
query: |-
select
board_name,
count(*) as ticket_count
from source
where board_name in $board_values
group by board_name

Keep the filters aligned so the detailed rows match the visible chart or metric. For charts, you can filter the clicked category with {{ x }} (the clicked display.x value).

Drilldown cells stay raw until display.columnFormats adds design cues for a column. Invoice numbers and other IDs stay ungrouped. Set useGrouping: true for counts, format: currency for money, percentSuffix: true or format: percent for rates, and format: date / datetime / time with dateStyle / timeStyle for timestamps. Chart-level rounding and percentSuffix do not carry into the drilldown.

How you open drilldown depends on the display type:

  • Chart and metric Query Widgets: click the widget body.
  • Table Query Widgets (display.type: table): use the expand control in the widget footer (Open drilldown). Dashboard table rows do not open drilldown on click.

If a table Query Widget has no drilldownQuery, the expand control shows a short message that no drilldown is available.

In the Query Widget editor preview (outside the dashboard), clicking a table row can still open drilldown when a drilldownQuery is set.

Viewers can use drilldowns through share links when the link has Allow drilldown into data enabled. The drilldown respects the link’s filter restrictions, so viewers only see rows within the allowed scope. This option requires a Professional-tier subscription or higher.

Shared viewers open table Query Widget drilldowns with the same expand control (Open drilldown) in the widget footer.

Preview runs immediately. Saved widgets update through the dashboard refresh pipeline.

For a Dataset source, the widget refreshes when that source updates and the dashboard is eligible to refresh.

For a Query Dataset source, the widget watches all base datasets used by the Query Dataset. The refresh interval is based on the most frequently updated source.

Example:

  • Tickets sync every 1 minute.
  • Ticket audit trail syncs every 15 minutes.
  • A Query Dataset joins both.
  • A Query Widget uses that Query Dataset.

The Query Widget uses a 1-minute target interval, and it can update when either related source changes.

Dashboards that have not been viewed recently may refresh less often. Opening the dashboard again makes it eligible for normal updates.

The Query Widget editor is friendly to AI-assisted drafting because the whole definition is YAML. Use Copy AI Instructions, then paste the current definition, relevant columns, and the preview error into a chat.

Useful prompt example:

I am editing a Resplendent Query Widget Definition.
Source:
- type: queryDataset
- source: 5a242d39-195c-41da-a99b-93b402a4325b
Columns:
- board_name: String
- ticket_number: Int64
- ticket_subject: String
- last_email_age_minutes: Float64
Goal:
Create a table showing the 100 oldest tickets by last outgoing email age.
Use the Query Widget boundary, so no joins or CTEs.

If the assistant writes a join, ask it to move that logic into a Query Dataset.

Preview works, but the saved widget keeps loading

Section titled “Preview works, but the saved widget keeps loading”

Preview runs in the editor. Saved widgets render in the background. If a saved widget keeps loading:

  1. Confirm the widget was saved.
  2. Check that the source dataset or Query Dataset has synced.
  3. Wait for the dashboard refresh worker to process the queued update.
  4. Reopen the editor and preview again to check for definition errors.

Widget Query must select from exactly one source relation named source

Section titled “Widget Query must select from exactly one source relation named source”

The SQL must use:

from source

Do not use the source UUID or a Query Dataset alias in a Query Widget.

Move the join into a Query Dataset, save it, then use that Query Dataset as the Query Widget source.

Quote dotted column names:

"board.name"

Make sure the SQL result uses the exact column name referenced by display.

display:
type: metric
value: ticket_count
select count(*) as ticket_count
from source

display.seriesBy requires exactly one display.y column

Section titled “display.seriesBy requires exactly one display.y column”

Long-form charts need one numeric y column plus seriesBy. Either drop extra y columns, or omit seriesBy and use multiple y columns for wide data.