Lewati ke konten utama
Versi: v0.0.75

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.

View / Build / Full modes (v0.0.58+)

The dashboard header carries a three-way mode picker. Selection persists per user.

ModeAudienceUI
ViewDefault. Directors, ops, casual viewers.Metabase-clean. No toolbar, no chat panel. Genuinely read-only — inline title editor, + Add Card, + From Template, Add page are all hidden.
BuildAnalysts authoring with AI. (Renamed from "Edit" in v0.0.61; renamed from "Chat" in v0.0.60.)Figma-Make-style chat-primary. Chat panel as a flex rail on the left, canvas on the right. Toolbar hidden — the chat panel is the edit surface.
FullLegacy MSTR-style.Toolbar Row 2 + sticky AI bar both visible; chat panel mounts as a collapsible overlay alongside. Transitional — planned to retire once Build covers every legacy capability.

Non-editors and kiosk users silently collapse to View regardless of preference. The mode key migrates automatically from the v0.0.56–v0.0.57 binary chat_primary flag.

Chat panel (Build mode)

The chat panel is a full authoring surface, not just a refinement bot. The LLM speaks in mutation envelopes (add_card, remove_card, update_card_config, change_card_type, set_period, add_filter, remove_filter, set_layout, clear_all_cards) which the platform applies through the same POST /api/dashboards/{id}/mutations pipeline regardless of whether they come from chat or the legacy toolbar — one config UPDATE plus one snapshot per batch.

Key affordances in the panel:

  • Artifact card at the top — sticky card surfacing the dashboard's title and latest revision number, Figma-Make's "file card" equivalent.
  • Period chip in the header (View + Build modes only) — 8 presets (All time, Last 7d/30d/90d, This month, Last month, This quarter, YTD).
  • @-mention autocomplete — type @ to cite datasets, dashboards, agents, recipes from the project. The handler re-validates each mention against list_project_assets for defense-in-depth; unauthorized citations drop silently.
  • Reasoning expander — assistant turns may carry an optional reasoning field surfaced as a collapsed <details> block.
  • Worked-with footer — assistant turns list every project asset they consulted, deduped across mentions, SQL inference, and LLM self-report. The same column powers compliance audit queries.
  • Follow-up chips — 2–3 one-click next-prompt suggestions below each turn. Clicking loads the prompt into the composer (no auto-send).
  • Refine-in-place — the Refine button on an inline chart pins its SQL as context for the next turn. The composer shows a "📌 Refining: <SQL>" chip with a clear . Send forwards the pinned SQL; the backend instructs the LLM to emit a delta rather than start fresh.
  • MOCK cards — cards flagged is_mock=true render against fabricated sample data with a 🎨 MOCK badge and a custom hover tooltip explaining why. The chat agent will never invent live results — when a question can't be answered with real data, the response is a mock card.
  • Inline chart (MSTR-style) — when ANSWER carries a SQL-backed answer, the chat panel renders the chart inside the message with a Refine + Add to dashboard action.
  • Vision (v0.0.63+) — paste (Cmd/Ctrl+V) or attach a screenshot. The turn forces a vision-capable model (openai:gpt-4o) so the LLM can mirror the visual layout from a Figma mockup or Tableau screenshot.
  • Agent mode (Tier C) — opt-in multi-step tool loop with read-only tools (search_assets, get_dataset_schema, run_sql_preview). The platform auto-enables agent mode for hallucination-prone request shapes. The Agent trace — N steps expander shows tool name + termination reason + elapsed.
  • Drag-resize — drag the right edge of the panel in Build mode to resize from 280 to 600 px. Width persists per user.

Chat history persists per dashboard in honeyframe.dashboard_chat_messages with mutation_ids + revision_number backrefs — every assistant turn is a navigable change-log entry.

Authoring

To create a dashboard:

  1. Open the project's Dashboards page.
  2. Click + New Dashboard, give it a name, optionally pick a Collection (folder) and a Template.
  3. The dashboard opens in design mode. The canvas is a free-form grid; the left rail is the card palette.
  4. + Add Card opens the card editor. Pick a card type, write the SQL, configure the visualization, save.
  5. Drag cards to position and resize. The layout (x, y, width, height) persists per card.
  6. Save. The dashboard is now visible to anyone with dashboard.read (legacy: viewer or 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:

TypeWhat it renders
tableSortable, paginated table over the SQL result.
bar, line, area, pie, donutStandard chart types over a {x, y, series?} mapping.
pie_3dTilted, exploded pie. Same {label, value} mapping as pie.
comboBar + line on a shared axis.
kpiSingle-metric card with optional comparison delta.
big_numberSingle value, no chart, no comparison. Largest type face — use for tile dashboards.
metric_gridMany KPIs packed into one card. Each row in the result becomes a tile (label, value).
sparklineInline trend line, no axis, optimized for narrow placement. {x, y} mapping.
bulletActual vs target with qualitative bands (actual, target, plus poor/good/great thresholds).
treemapNested rectangles sized by a metric. {label, size} mapping.
calendar_heatmapDay-of-year grid colored by a daily value. {day, value} mapping.
pivotPivot table — choose rows, columns, measures, aggregation. Heatmap shading and conditional formatting available.
geo_scatterMap with pins; size and color encode metrics. {label, lat, lon, size?} mapping.
choroplethRegion-shaded map. Built-in Indonesia province boundaries; province names normalized across upstream GeoJSON sources.
section_dividerSection header rendered as a clean divider in view mode (no chrome). Content lives in card_config.label, not in SQL.
textStatic markdown text block.
notebook_cellEmbeds the cached output of a notebook cell — the notebook must have been executed for the cell to render.
embedIframe 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. 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.

The interval is exposed in the Edit Card sidebar (v0.0.40) as an Auto-refresh dropdown with choices Off / 10s / 30s / 1m / 5m / 10m / 30m. Empty = off. Storage is card_config.refresh_interval_seconds; the setInterval wiring and LiveBadge gate on > 0. Programmatic creation (via POST /api/dashboards/{id}/cards) sets the same field directly.

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 typeURLAuth
PrivateSame as the platform URLPlatform login required; recipient must have dashboard.read.
Public link/public/dashboards/{id}?token=...None — anyone with a valid token views it.

Public sharing is per-link, not per-dashboard. Open Share → Public Links to mint a new link. Each link is a row in dashboard_public_links carrying:

  • An optional label so you remember who you sent it to.
  • An optional expires_days value (the link 410s past expiry).
  • A view counter + last-viewed timestamp so you can audit usage.
  • A revoke action — revocation is immediate; the URL 410s afterward.

When you mint a new link, the raw token URL is shown once in a one-time reveal panel. Copy it and close the modal — Honeyframe never re-displays the bare token (only a masked form), and tokens are sha256-hashed at rest. If you lose the URL, revoke the link and mint a new one.

The legacy is_public BOOLEAN model still works for dashboards already shared that way — those URLs continue to resolve. New shares should use per-link tokens.

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 audienceExecutive, Operations, Engineering.
  • By domainSales, Operations, Inventory.
  • By lifecycleProduction, 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.

Admin override (v0.0.47+): users with role='admin' can read and edit every dashboard in any org they belong to, regardless of ownership or share rows. Cross-tenant access is still gated by org-intersection — the override only applies within the orgs the admin is already a member of. The dashboard listing gains an All tab for admins (alongside Owned and Shared) that surfaces every dashboard in their orgs in one view. Non-admins who lose access to a dashboard now see a friendly error banner with a Try again button instead of a silent redirect.

Override governance (v0.0.48+): every admin-override read or edit emits an audit row tagged with the *.admin_override_* action and fires a dedicated audit.admin_override webhook event alongside the standard audit.event firehose, so compliance teams can subscribe to overrides as a standalone channel. The dashboard toolbar shows a rose 🛡️ "Admin access · audited" badge whenever the response carries is_admin_override=true. The same shortcut now also covers notebooks and recipes (cross-project reads via the same audit + webhook pattern; cross-org admins still 404). When an admin pastes a wrong-org dashboard / dataset / notebook / agent URL, the page renders a NotFoundBanner with org-switcher buttons rather than silently redirecting.

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 snapshotPOST /api/dashboards/{id}/revisions with an optional change_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.

Display options (v0.0.62+)

Card-type-specific knobs live in a per-card-type display_options registry. The chat agent emits them via update_card_config mutations; the LLM proposes the change as a PlanCard, the user clicks Yes, the mutation fires (commit-on-yes — display-option toggles never auto-apply silently).

OptionCard typesNotes
color_scalegeo_scatter, choroplethviridis, plasma, rdylgn, etc.
number_formatkpi, big_number, metric_gridSmall DSL — "0.0%", "$#,##0".
sort_by, sort_directiontable, pivotMulti-column sort supported on pivot.
stackbar, areagroup, stack, percent-stack. Percent-stacked tooltip surfaces both the raw and percentage value.

Width buckets (v0.0.61+)

Card widths are stored as one of three semantic buckets rather than raw pixel widths:

Bucketsize_xUse
small8KPIs, sparklines
medium12Paired charts, half-width tables
full24Tables, maps, choropleths

The editor's resize handle snaps to these buckets. Existing dashboards backfill width_bucket from size_x on first read. Buckets flow through the JSX/Figma-Make exporter, the Metabase importer, and every LLM prompt that mentions card sizing.

BI interop (v0.0.59+)

DirectionEndpointTrigger
Honeyframe → SVGGET /api/dashboards/{id}/export-svgExport → Export to Figma (SVG) menu entry. Labeled placeholder rects in the 24-col grid, multi-page stacking. No live chart render — designers redraw in their tool.
Honeyframe → JSX(in the same Export menu)JSX / Figma-Make exporter (v0.0.61).
Metabase → HoneyframePOST /api/dashboards/import-from-metabaseList page → Import → Metabase. Pulls dashboards + dashcards, scales 18-col → 24-col grid. Native (raw-SQL) questions carry their query; MBQL questions land as [Skipped] placeholder cards.
Figma Make → HoneyframePOST /api/dashboards/import-from-figma-makeList page → Import → Figma Make. Heuristic JSX parser produces a dashboard skeleton with empty SQL. Best-effort — skipped-block count surfaced in the modal.

For zero-code embed, the existing public-link URLs (/public/dashboards/{id}?token=…) work in Figma Make / Notion / Confluence iframes.

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.

In v0.0.41 the prompt was hardened with two few-shot worked examples (a wide scorecard using metric_grid + section_divider + a horizontal bar comparison; and a wide-monthly LATERAL VALUES reshape that pivots 12 month columns into a line chart). Concrete examples drive the LLM far more reliably than abstract rules — the prior prompt tended to plot every column individually on wide tables, or fan 12 monthly columns out side-by-side. The same release added an empty-state CTA on the dashboard detail page: when canEdit && cards.length === 0, a "Generate dashboard with AI" button appears that calls /generate directly (bulk — 8-12 cards in one shot) so operators can bulk-seed an empty dashboard without going back to the list view.

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).

EndpointDescription
GET /api/dashboards/card-templatesList templates visible to the caller's org.
POST /api/dashboards/card-templatesSave the current card as a template.
DELETE /api/dashboards/card-templates/{id}Remove (owner only).
POST /api/dashboards/card-templates/{id}/insertInsert 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:

  1. Author the dashboard on the Platform (platform.your-domain.com/dashboards).
  2. Add it to a publishable app via Publish Manager.
  3. The published version lands on the SaaS surface (app.your-domain.com/dashboards/<slug>).
  4. 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

EndpointDescription
GET /api/dashboardsList dashboards visible to the caller.
GET /api/dashboards/templatesList built-in templates.
GET /api/dashboards/{id}Full dashboard config including cards, layout, filters, pages.
POST /api/dashboardsCreate.
PATCH /api/dashboards/{id}Update title, description, collection, visibility.
DELETE /api/dashboards/{id}Delete.
POST /api/dashboards/{id}/duplicateClone the dashboard (cards + layout + filters; not shares).
POST /api/dashboards/{id}/cardsAdd 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}/executeRun one card.
POST /api/dashboards/{id}/execute-allRun all cards on the active page in parallel.
POST /api/dashboards/{id}/filter-optionsDistinct values for a filter column.
POST /api/dashboards/{id}/pagesAdd 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}/layoutSave card positions and sizes.
GET /api/dashboards/{id}/sharesList share links.
POST /api/dashboards/{id}/sharesCreate a public or private share link.
DELETE /api/dashboards/{id}/shares/{share_id}Revoke a share.
POST /api/dashboards/collectionsCreate a collection.
GET /api/dashboards/public/{share_id}View a public dashboard (no auth).
POST /api/dashboards/generateAI-generate from a natural-language description (+ optional screenshot data URL). Experimental.
POST /api/dashboards/ai-chatChat-style dashboard refinement. Experimental.
POST /api/dashboards/{id}/suggest-parametersHeuristic extraction of hardcoded literals into {{params}}. Editor-gated.
POST /api/dashboards/{id}/apply-suggestionsAtomically rewrite card SQL + insert parameters into config.
GET /api/dashboards/{id}/revisionsList revisions (lightweight, no snapshot payload).
GET /api/dashboards/{id}/revisions/{n}Full snapshot for a revision.
POST /api/dashboards/{id}/revisionsForce a manual snapshot.
POST /api/dashboards/{id}/revisions/{n}/restoreAtomically restore the dashboard to a prior revision.
GET /api/dashboards/card-templatesList org-scoped card templates.
POST /api/dashboards/card-templatesSave a card as a reusable template.
DELETE /api/dashboards/card-templates/{id}Remove a card template (owner only).
POST /api/dashboards/card-templates/{id}/insertInsert a card template into a dashboard.

Performance

Heavy dashboards (10+ cards over large datasets) benefit from:

  • Materialized intermediates — replace a prepare → group_by chain 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 tablestable cards default to 100 rows. Lower the page size in card config for at-a-glance dashboards.
  • Parallel executionexecute-all parallelises 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_at rewrites every card whose SQL SELECTs a created_at column. 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.