# How to Create Dashboard Applications in Retool

- Tool: Retool
- Difficulty: Beginner
- Time required: 30-40 min
- Compatibility: Retool Cloud and Self-hosted
- Last updated: March 2026

## TL;DR

Build Retool dashboards by combining Statistics components (for KPI numbers), Chart components (for trends), and Table components (for drill-down). Bind all components to queries that share a common date filter. Use query auto-refresh intervals for near-real-time updates. Structure your layout with Container components and a fixed 12-column grid.

## Build a Data Dashboard with KPIs, Charts, and Filters

Retool is especially well-suited for building internal dashboards — it connects directly to your databases without an intermediate API layer, updates in real time, and requires no frontend code to lay out professional-looking charts and KPI metrics.

A typical Retool dashboard has three layers: (1) KPI numbers at the top using Statistic components, (2) trend charts in the middle using Chart components, and (3) a detailed table at the bottom for drill-down. A shared date range filter connects all three layers.

This tutorial builds a complete operations dashboard with revenue metrics, a trend line chart, and an orders table — all filtered by a shared date range picker and auto-refreshing every 60 seconds.

## Before you start

- A Retool app connected to a SQL database with time-series data
- Basic familiarity with adding components to the Retool canvas
- A database table with at least a date column and one or more numeric metric columns

## Step-by-step guide

### 1. Create the KPI aggregation query

Create a SQL query named 'getKPIs'. This query returns a single row with aggregate metrics for the selected date range. Reference the date range picker using {{ dateRangePicker1.value }} — Retool date pickers return an array [startDate, endDate]. Enable 'Run when inputs change' so the query re-runs automatically when the date range changes. Do not run on page load from this query — let the date picker's default value trigger it.

```
-- getKPIs query
SELECT
  COUNT(*) AS total_orders,
  SUM(amount) AS total_revenue,
  AVG(amount) AS avg_order_value,
  COUNT(DISTINCT customer_id) AS unique_customers,
  SUM(CASE WHEN status = 'returned' THEN 1 ELSE 0 END)::float / COUNT(*) * 100 AS return_rate
FROM orders
WHERE
  created_at >= {{ dateRangePicker1.value[0] }}
  AND created_at <= {{ dateRangePicker1.value[1] }};
```

**Expected result:** getKPIs returns a single aggregate row when the date range is selected.

### 2. Add a Date Range Picker and connect it to all queries

Drag a Date Range Picker component to the top of the canvas. Set its Default start date to the first day of the current month: {{ moment().startOf('month').toISOString() }}. Set its Default end date to today: {{ moment().toISOString() }}. All queries that filter by date should reference {{ dateRangePicker1.value[0] }} and {{ dateRangePicker1.value[1] }}. With 'Run when inputs change' enabled on each query, changing the date range instantly refreshes all charts and KPIs.

**Expected result:** Date Range Picker shows current month. Changing the date range triggers all queries to re-run.

### 3. Add KPI Statistic components at the top

Drag 4 Statistic components into a horizontal row at the top of the canvas. For each one, set the Statistic value to a field from getKPIs: {{ getKPIs.data[0].total_revenue }}, {{ getKPIs.data[0].total_orders }}, etc. Set the Label text to a human-readable name ('Total Revenue', 'Total Orders'). Use the Prefix field for currency symbols ('$') and the Suffix field for units ('%' for return rate). Set the Format to 'Number' and configure decimal places. Statistics components also support a trend delta — bind it to compare current vs prior period.

```
// Statistic component values:
// Total Revenue: {{ getKPIs.data[0]?.total_revenue?.toLocaleString('en-US', { maximumFractionDigits: 0 }) }}
// Total Orders: {{ getKPIs.data[0]?.total_orders }}
// Avg Order Value: {{ getKPIs.data[0]?.avg_order_value?.toFixed(2) }}
// Return Rate: {{ getKPIs.data[0]?.return_rate?.toFixed(1) }}
```

**Expected result:** Four KPI cards display formatted metrics from the getKPIs query, updating when the date range changes.

### 4. Add a trend Chart below the KPIs

Create a second query 'getRevenueTrend' that groups revenue by day or week within the selected date range. Drag a Chart component below the KPI row. Set its Data source to {{ getRevenueTrend.data }}. Configure X axis to the date column and Y axis to the revenue column. Set the chart Type to 'Line' and enable 'Show area'. Add a title in the Inspector. The chart will automatically refresh with the date range because getRevenueTrend references the same dateRangePicker1 inputs.

```
-- getRevenueTrend query
SELECT
  DATE_TRUNC('day', created_at) AS period,
  SUM(amount) AS revenue,
  COUNT(*) AS order_count
FROM orders
WHERE
  created_at >= {{ dateRangePicker1.value[0] }}
  AND created_at <= {{ dateRangePicker1.value[1] }}
GROUP BY 1
ORDER BY 1;
```

**Expected result:** A line chart displays daily revenue trends for the selected date range.

### 5. Add a top-items breakdown chart

Create a third query 'getTopCategories' that returns revenue by category. Add a second Chart component set to 'Bar' chart type showing category breakdowns. Use the Group By feature in the Chart Inspector to aggregate data if the query returns raw rows. For a horizontal bar chart, swap X and Y axes and set the Orientation to 'Horizontal' in the chart Inspector. This gives users a quick visual breakdown without reading a table.

**Expected result:** A horizontal bar chart shows revenue by category, updating with the date range filter.

### 6. Add a detail Table for drill-down

At the bottom of the dashboard, add a Table component bound to a 'getOrders' query that shows individual orders with pagination. Add a search Text Input above the table and reference it in the query: WHERE ... AND (customer_name ILIKE {{ '%' + search.value + '%' }} OR order_id::text ILIKE {{ '%' + search.value + '%' }}). Enable server-side pagination in the query using LIMIT and OFFSET tied to {{ table1.pageSize }} and {{ table1.pageIndex * table1.pageSize }}.

**Expected result:** A paginated, searchable orders table appears below the charts.

### 7. Configure auto-refresh for near-real-time updates

In each query's settings, scroll to the Advanced section and find 'Polling interval' or 'Auto-run'. Set all dashboard queries (getKPIs, getRevenueTrend, getTopCategories) to auto-run every 60 seconds. This keeps the dashboard fresh without requiring the user to manually refresh. For true real-time data, see the WebSocket tutorial — polling every 60 seconds covers most internal dashboard use cases at minimal query cost.

**Expected result:** Dashboard auto-refreshes all KPIs and charts every 60 seconds without user interaction.

## Complete code example

File: `SQL Query: getKPIs with date comparison`

```sql
-- Full getKPIs query with current period + prior period comparison
-- Returns both periods for delta/trend indicators on Statistic components

WITH current_period AS (
  SELECT
    COUNT(*) AS orders,
    COALESCE(SUM(amount), 0) AS revenue,
    COALESCE(AVG(amount), 0) AS avg_order_value,
    COUNT(DISTINCT customer_id) AS unique_customers
  FROM orders
  WHERE
    created_at >= {{ dateRangePicker1.value[0] }}
    AND created_at < {{ dateRangePicker1.value[1] }}
    AND status != 'cancelled'
),
prior_period AS (
  SELECT
    COUNT(*) AS orders,
    COALESCE(SUM(amount), 0) AS revenue
  FROM orders
  WHERE
    created_at >= {{ moment(dateRangePicker1.value[0]).subtract(
      moment(dateRangePicker1.value[1]).diff(moment(dateRangePicker1.value[0]), 'days'),
      'days'
    ).toISOString() }}
    AND created_at < {{ dateRangePicker1.value[0] }}
    AND status != 'cancelled'
)
SELECT
  c.orders,
  c.revenue,
  c.avg_order_value,
  c.unique_customers,
  c.revenue - p.revenue AS revenue_delta,
  ROUND((c.revenue - p.revenue) / NULLIF(p.revenue, 0) * 100, 1) AS revenue_pct_change,
  c.orders - p.orders AS orders_delta
FROM current_period c, prior_period p;
```

## Common mistakes

- **Building a dashboard where each KPI stat runs its own separate query, leading to 10+ queries on page load** — undefined Fix: Combine all KPI metrics into a single aggregation query that returns one row with all metrics. Reference individual fields as {{ getKPIs.data[0].metric_name }}.
- **Using dateRangePicker1.value without checking if it has been set, causing null reference errors** — undefined Fix: Set a default value on the Date Range Picker (current month start/end) so it always has a value when queries run on page load.
- **Setting auto-refresh on every query including detail tables, causing unnecessary database load** — undefined Fix: Only auto-refresh the top-level aggregate queries. Detail tables and drill-down queries should only refresh when explicitly triggered by user interaction.
- **Displaying raw numbers from queries without formatting, showing values like 1250000.456789** — undefined Fix: Use JavaScript number formatting in Statistic values: {{ getKPIs.data[0].revenue.toLocaleString('en-US', { maximumFractionDigits: 0 }) }} or a transformer that pre-formats all values.

## Best practices

- Use a single Date Range Picker component referenced by all queries — do not create separate date filters per query
- Always add a loading state to your KPI cards: {{ getKPIs.isFetching }} can drive a Spinner component near the title
- Format numbers in Statistic components using toLocaleString() rather than raw values — '1,250,000' is more readable than '1250000'
- Avoid loading too many queries on page load — use 'Run when inputs change' so queries only fire when the date filter has a value
- Group related charts in Container components to create visual sections and add section headings with Text components
- For public-facing dashboards, consider caching queries with a 5-minute TTL to reduce database load

## Frequently asked questions

### Can I embed a Retool dashboard in an external website?

Yes — Retool supports embedding apps in iframes using Embed mode (Business plan and above). Generate an embed URL from the app's Share menu, configure allowed domains in Settings, and use the iframe src with an auth token. The embedded dashboard respects your Retool permissions and can accept URL parameters for pre-filtering.

### How do I add drill-down functionality to a chart in a Retool dashboard?

Use the Chart component's {{ chart1.selectedPoints }} property. When a user clicks a bar or point, selectedPoints is populated with the clicked data. Configure a Temporary State variable to store the selection and use it as a filter parameter in a detail query. See the 'How to create custom charts in Retool' tutorial for the full implementation.

### What is the maximum number of components I can add to a Retool dashboard before performance degrades?

There is no hard limit, but dashboards with more than 40-50 components or 15+ simultaneous queries may experience slow load times. Retool's Debug Panel Performance tab grades your app's component count. Use multipage apps to split large dashboards, and cache slow queries with a TTL to reduce database pressure.

---

Source: https://www.rapidevelopers.com/retool-tutorial/how-to-create-dashboard-applications-in-retool
© RapidDev — https://www.rapidevelopers.com/retool-tutorial/how-to-create-dashboard-applications-in-retool
