Dashboards
A dashboard is a layout of cards that visualize one or more SQL queries. Each card is a chart, table, KPI, or text block driven by a SQL statement against the project's datasets. Operators build dashboards in a drag-and-drop editor, organize them into pages and collections, apply dashboard-level filters, and share them with users or via public links.
Authoring
To create a dashboard:
- Open the project's Dashboards page.
- Click + New Dashboard, give it a name, optionally pick a Collection (folder) and a Template.
- The dashboard opens in design mode. The canvas is a free-form grid; the left rail is the card palette.
- + Add Card opens the card editor. Pick a card type, write the SQL, configure the visualization, save.
- Drag cards to position and resize. The layout (
x,y,width,height) persists per card. - Save. The dashboard is now visible to anyone with
dashboard.read(legacy:vieweror higher).
Dashboards can have multiple pages (tabs). Pages are independent layouts; cards do not move between pages without an explicit duplicate.
Card types
Each card is one of:
| Type | What it renders |
|---|---|
table | Sortable, paginated table over the SQL result. |
bar, line, area, pie, donut | Standard chart types over a {x, y, series?} mapping. |
pie_3d | Tilted, exploded pie. Same {label, value} mapping as pie. |
combo | Bar + line on a shared axis. |
kpi | Single-metric card with optional comparison delta. |
big_number | Single value, no chart, no comparison. Largest type face — use for tile dashboards. |
metric_grid | Many KPIs packed into one card. Each row in the result becomes a tile (label, value). |
sparkline | Inline trend line, no axis, optimized for narrow placement. {x, y} mapping. |
bullet | Actual vs target with qualitative bands (actual, target, plus poor/good/great thresholds). |
treemap | Nested rectangles sized by a metric. {label, size} mapping. |
calendar_heatmap | Day-of-year grid colored by a daily value. {day, value} mapping. |
pivot | Pivot table — choose rows, columns, measures, aggregation. Heatmap shading and conditional formatting available. |
geo_scatter | Map with pins; size and color encode metrics. {label, lat, lon, size?} mapping. |
choropleth | Region-shaded map. Built-in Indonesia province boundaries; province names normalized across upstream GeoJSON sources. |
section_divider | Section header rendered as a clean divider in view mode (no chrome). Content lives in card_config.label, not in SQL. |
text | Static markdown text block. |
notebook_cell | Embeds the cached output of a notebook cell — the notebook must have been executed for the cell to render. |
embed | Iframe embed — for external BI tiles or other Honeyframe surfaces. |
The bullet, calendar_heatmap, metric_grid, sparkline, big_number, treemap, pie_3d, geo_scatter, and section_divider types were added in v0.0.38. section_divider was promoted to a first-class card in v0.0.39 (no SQL, no chrome — card_config = { label, level: h1|h2|h3 }); the v0.0.38 text-card-as-divider workaround is now a back-compat path only.
Each card has a card_config object whose shape varies by type — column mappings, color palettes, axis labels, formatters. The card editor renders a form against the schema; programmatic creation should mirror what the form generates.
Conditional formatting
KPI cards and pivot cells can apply per-value formatting rules: a comparison (>, <, BETWEEN) against a threshold sets the cell's text color, background, or icon. Useful for "above target = green" / "below SLA = red" tile dashboards without writing CASE in the SQL.
In v0.0.39 KPI conditional formatting gained semantic thresholds and a direction flag:
{
"kpi_thresholds": { "warn_below": 0.95, "danger_below": 0.80, "target": 1.0 },
"kpi_direction": "higher_is_better"
}
kpi_direction flips the comparison so error-rate or latency cards go green when the value is low. The configured target renders as a small target N line under the value, making threshold-driven dashboards self-documenting. The legacy { red, orange, green } shape is still honored.
Theme override
A dashboard can pin a color palette that all its cards inherit (card_config.color_theme). Override at the dashboard level once instead of per card.
Live auto-refresh
Each card can opt into a refresh interval (10s minimum). The card shows a Live badge while polling. The dashboard does not refresh as a whole — refresh is per-card so a slow query doesn't block the rest of the layout.
Reordering pages
Pages support neighbor-swap reorder via PATCH /api/dashboards/{id}/pages/{page_number}/swap. Drag in the UI; the swap is single-step (move adjacent). Long-distance moves are repeated swaps.
Parameters
A parameter is a named input bound at the dashboard level (not the card level). Cards reference it as {{name}} in their SQL; the toolbar exposes one input per parameter; the platform substitutes a named SQL bind (:p_<name>) at execution time. Type-safe and free from string-injection.
{
"parameters": [
{ "name": "min_amount", "type": "number", "default": 100 },
{ "name": "region", "type": "text", "default": "APAC" }
]
}
SELECT region, SUM(amount) AS revenue
FROM {{ ref('orders') }}
WHERE region = {{region}} AND amount >= {{min_amount}}
GROUP BY region
An empty toolbar input falls through to the configured default rather than binding NULL — clearing an input reverts to the default. Cards that reference an unbound parameter without a default skip silently rather than crashing the whole dashboard.
Two endpoints on the parameter surface:
POST /api/dashboards/{id}/suggest-parameters— scans every card's SQL for hardcoded literals (WHERE col = 'literal',LIMIT N,BETWEEN 'a' AND 'b') that look like operator filter values. Skips primary-key columns, projection literals, and literals already inside{{tokens}}. Returns one suggestion per literal with the list of card_ids that share it.POST /api/dashboards/{id}/apply-suggestions— atomically rewrites the card SQL and inserts the parameter into the dashboard config.
Parameters substitute before the filter wrapper runs, so the bind names are disjoint (p_<name> vs. f<i>_*) and both can coexist on the same query.
Parameters and Filters are independent surfaces — parameters are operator-defined inputs that the SQL author opted into via {{name}}; filters apply automatically based on column matches. Use parameters for "reusable knob the SQL author shaped around" and filters for "this dashboard scopes by region across every card".
Filters
A dashboard can declare filters that flow into every card's SQL. Each filter has a column, an operator (=, !=, >, <, >=, <=, IN, BETWEEN, LIKE, IS NULL, IS NOT NULL), and a value.
The platform rewrites each card's SQL at query time to apply matching filters as WHERE clauses. A card opts out of a filter by name in its config — useful for "context" cards that should always show the full picture.
In v0.0.39 the toolbar wires through end-to-end: any filter input change debounces 300 ms then re-runs every card on the active page. The active-filters bar shows a total count across date / equality / widget / drill-down sources; Clear all wipes everything in one click. Each card carries a ▼N badge showing how many active filters actually hit that card's SQL (mirrors the backend's word-boundary check), so "did my filter even apply here?" is now visible at a glance.
For dropdown / autocomplete filter inputs, POST /api/dashboards/{id}/filter-options returns the distinct values for the filter column from the underlying dataset.
Active-filters bar
When any dashboard filter has a non-default value, an active-filters bar appears above the canvas with one chip per applied filter. Click a chip to clear it; clear-all clears every filter. The bar is single-source — both dashboard-level filters and card drill-downs surface here.
Time-range presets and URL persistence
The dashboard time filter exposes presets (Today, Last 7 days, Last 30 days, MTD, QTD, YTD) plus a custom range. The active range is encoded in the URL query string, so a dashboard URL like /dashboards/42?range=30d&filter.region=APAC shares the exact view to a teammate without them re-applying anything.
Global filter widgets
A project-level filter widget is a saved filter shape (column, operator, default) that any dashboard in the project can pull in. Edit the shape once via project settings → Filters; every dashboard referencing that widget picks up the new default. Widgets exist alongside per-dashboard filters; both flow into card SQL.
Sharing
Dashboards are organization-private by default. To extend access, open Share:
| Share type | URL | Auth |
|---|---|---|
| Private | Same as the platform URL | Platform login required; recipient must have dashboard.read. |
| Public link | /public/dashboards/{share_id} | None — anyone with the link views it. |
Each share link is a row in dashboard_shares with a unique share_id. Revoke from the same modal — revocation is immediate; the URL returns 410 Gone afterward.
Public visibility is org-admin opt-in. There is no public path until an org admin enables public sharing for the project.
Collections
Collections are folders for organizing dashboards. They nest (parent → child → grandchild) and a dashboard belongs to exactly one collection (or none, in which case it sits at the project root).
Use collections for:
- By audience —
Executive,Operations,Engineering. - By domain —
Sales,Operations,Inventory. - By lifecycle —
Production,Drafts,Archive.
Collections affect organization only. They do not gate access; permissions are still per-dashboard.
Permissions
Two permission strings cover the dashboard surface:
dashboard.read— view a dashboard and run its cards.dashboard.edit— modify layout, cards, filters, sharing.
The legacy require_role("viewer") / require_role("editor") checks are honored during the migration. See Permissions Reference.
Card execution
Each card runs its SQL independently. The platform caches results in-memory per process with a TTL — repeat loads of the same card with the same filter set hit the cache. Cache size is bounded (~500 entries per process) and is not shared across replicas, so a horizontally-scaled deployment may see different cache hit rates per request depending on which replica handles it.
Two execution endpoints:
POST /api/dashboards/{id}/cards/{card_id}/execute— run one card with the given filters.POST /api/dashboards/{id}/execute-all— run every card on the active page in parallel. Returns when all cards have finished or errored.
Masking
PII masking from the Datasets layer applies to dashboard card output. Masking is applied at the Python layer after SQL execution, before JSON serialization — sensitive columns are redacted in the response, but the underlying SELECT still reads the raw column. Operators without data.read_unmasked see only the masked value.
Version history
Every dashboard has a per-revision history drawer (v0.0.39). Operators browse past revisions, preview them in-place, and restore any prior revision atomically — the live dashboard rolls back to that snapshot in one click, with a pre-restore safety snapshot taken automatically.
Storage lives in honeyframe.dashboard_revisions: one row per revision with the full dashboard state (cards + pages + config) as a JSONB blob, the author user, an optional change_note, and a gapless revision_number per dashboard.
Two snapshot triggers:
- Editor autosave — rate-limited to one revision per 30s per author. Keeps the history readable; rapid scrubbing in the editor doesn't generate noise.
- Manual snapshot —
POST /api/dashboards/{id}/revisionswith an optionalchange_note. Force-runs regardless of the rate limit.
Two list/read endpoints:
GET /api/dashboards/{id}/revisions— lightweight list (no snapshot payload), most-recent first, capped at 200 rows.GET /api/dashboards/{id}/revisions/{n}— full snapshot, used by the preview pane and the diff drawer.
The diff drawer renders side-by-side the two revisions selected (current vs. picked, or any two from the list).
For dashboards that change rarely, see Scheduled snapshots below — the editor only snapshots on edit, so a dashboard untouched for a week has no recovery point without scheduled checkpointing.
Scheduled snapshots
A new scenario step type snapshot_dashboards (v0.0.39, see Scheduler) wires nightly or any-cadence checkpointing for every dashboard in a project. Step config:
steps:
- type: snapshot_dashboards
project_id: 42 # defaults to scenario's project_id
skip_hours: 12 # don't re-snapshot dashboards already snapshotted within 12h
max_dashboards: 500
change_note: "Daily auto-snapshot"
With this active and a daily cron, every dashboard has a recovery point within 24h regardless of edit activity.
AI generation
Two experimental endpoints lower the cost of building a dashboard from scratch:
POST /api/dashboards/generate— provide a natural-language description and optionally a screenshot (paste / upload a Figma or PowerPoint mockup); the platform proposes a dashboard with several cards. Image input forces a vision-capable model (default:openai:gpt-4o); the LLM mirrors the visual layout while staying schema-grounded (it won't invent tables just because labels appear in the screenshot). Cmd/Ctrl+V on the prompt pastes from clipboard. (Screenshot support added in v0.0.39.)POST /api/dashboards/ai-chat— chat-style refinement of an existing dashboard. "Add a bar chart showing weekly signups by region" → patches the dashboard with the new card.
These hit the configured LLM connector. They are scaffolding, not authoring — review the generated SQL before publishing.
Drilldown and selection
Click a chart series or a table row to drill down — the dashboard re-runs every card filtered by (column = clicked_value). The originating card gets a purple-violet ring while the drill is active (cause-effect visible at a glance); the toolbar shows a Drilled: col = value from <card title> pill. Esc clears the drill (Looker / Tableau muscle memory).
In edit mode, shift-click toggles a card into a multi-selection (teal ring). A floating action bar (bottom-center) appears with Hide / Show / Delete for bulk operations on the selection. Plain click with a non-empty selection replaces it; Esc clears it; exiting edit mode clears stale selections.
Templates
Two distinct concepts live under the "templates" word:
Built-in dashboard templates are the fastest way to start. + New Dashboard → From template lists the catalog (GET /api/dashboards/templates); pick one, point it at a dataset, and the platform clones the template with the dataset's column names substituted into the cards' SQL. Custom dashboard templates are admin-only — no end-user "save as template" flow today.
Card templates library (v0.0.39) is org-scoped and end-user-driven. Save any well-shaped card from the editor as a reusable template; insert it into any other dashboard in the same org with one click. Storage: honeyframe.dashboard_card_templates with (template_id, org_id, owner_user_id, name, card_type, sql_query, card_config, size_x, size_y).
| Endpoint | Description |
|---|---|
GET /api/dashboards/card-templates | List templates visible to the caller's org. |
POST /api/dashboards/card-templates | Save the current card as a template. |
DELETE /api/dashboards/card-templates/{id} | Remove (owner only). |
POST /api/dashboards/card-templates/{id}/insert | Insert the template into a dashboard at given coordinates. |
Publishing to a SaaS app
Dashboards can be published as part of a Honeyframe app — a customer-facing bundle that lives on the SaaS surface. Publishing assigns the dashboard a stable URL on the SaaS frontend and freezes its config so further edits don't immediately propagate.
Lifecycle:
- Author the dashboard on the Platform (
platform.your-domain.com/dashboards). - Add it to a publishable app via Publish Manager.
- The published version lands on the SaaS surface (
app.your-domain.com/dashboards/<slug>). - Subsequent edits on the Platform stay in draft; promote with Publish.
The author-on-PaaS, view-on-SaaS split keeps the design surface separate from the live audience surface.
API reference
| Endpoint | Description |
|---|---|
GET /api/dashboards | List dashboards visible to the caller. |
GET /api/dashboards/templates | List built-in templates. |
GET /api/dashboards/{id} | Full dashboard config including cards, layout, filters, pages. |
POST /api/dashboards | Create. |
PATCH /api/dashboards/{id} | Update title, description, collection, visibility. |
DELETE /api/dashboards/{id} | Delete. |
POST /api/dashboards/{id}/duplicate | Clone the dashboard (cards + layout + filters; not shares). |
POST /api/dashboards/{id}/cards | Add a card. |
PATCH /api/dashboards/{id}/cards/{card_id} | Update card SQL, type, config, title. |
DELETE /api/dashboards/{id}/cards/{card_id} | Remove a card. |
POST /api/dashboards/{id}/cards/{card_id}/execute | Run one card. |
POST /api/dashboards/{id}/execute-all | Run all cards on the active page in parallel. |
POST /api/dashboards/{id}/filter-options | Distinct values for a filter column. |
POST /api/dashboards/{id}/pages | Add a page. |
PATCH /api/dashboards/{id}/pages/{page_number} | Rename a page. |
DELETE /api/dashboards/{id}/pages/{page_number} | Remove a page. |
PATCH /api/dashboards/{id}/layout | Save card positions and sizes. |
GET /api/dashboards/{id}/shares | List share links. |
POST /api/dashboards/{id}/shares | Create a public or private share link. |
DELETE /api/dashboards/{id}/shares/{share_id} | Revoke a share. |
POST /api/dashboards/collections | Create a collection. |
GET /api/dashboards/public/{share_id} | View a public dashboard (no auth). |
POST /api/dashboards/generate | AI-generate from a natural-language description (+ optional screenshot data URL). Experimental. |
POST /api/dashboards/ai-chat | Chat-style dashboard refinement. Experimental. |
POST /api/dashboards/{id}/suggest-parameters | Heuristic extraction of hardcoded literals into {{params}}. Editor-gated. |
POST /api/dashboards/{id}/apply-suggestions | Atomically rewrite card SQL + insert parameters into config. |
GET /api/dashboards/{id}/revisions | List revisions (lightweight, no snapshot payload). |
GET /api/dashboards/{id}/revisions/{n} | Full snapshot for a revision. |
POST /api/dashboards/{id}/revisions | Force a manual snapshot. |
POST /api/dashboards/{id}/revisions/{n}/restore | Atomically restore the dashboard to a prior revision. |
GET /api/dashboards/card-templates | List org-scoped card templates. |
POST /api/dashboards/card-templates | Save a card as a reusable template. |
DELETE /api/dashboards/card-templates/{id} | Remove a card template (owner only). |
POST /api/dashboards/card-templates/{id}/insert | Insert a card template into a dashboard. |
Performance
Heavy dashboards (10+ cards over large datasets) benefit from:
- Materialized intermediates — replace a
prepare → group_bychain in the source Flow with a single dbt-built aggregate dataset. Card queries against the aggregate are sub-second. - Result caching — the in-memory cache covers identical (SQL, filter) pairs. Cards that share a dataset and filter set share a cache hit.
- Pagination on tables —
tablecards default to 100 rows. Lower the page size in card config for at-a-glance dashboards. - Parallel execution —
execute-allparallelises cards on the same page; the wall-clock is bounded by the slowest card, not the sum.
Gotchas
- Cache is per-process. Behind a load balancer, two requests for the same dashboard may take different cache paths. Don't rely on cache hit rates for SLA budgeting.
- Filter rewriting is column-name based. A filter on
created_atrewrites every card whose SQLSELECTs acreated_atcolumn. Cards that should ignore the filter must opt out by name in their config. - Public dashboards cannot filter by user. They have no caller identity. For row-level access control, use private shares.
- Notebook cells render cached output. If the notebook hasn't been run, the card is empty until the next notebook execution.
- Custom templates are admin-only. Users cannot promote their own dashboards to templates today.