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.
| Mode | Audience | UI |
|---|---|---|
| View | Default. 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. |
| Build | Analysts 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. |
| Full | Legacy 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 againstlist_project_assetsfor defense-in-depth; unauthorized citations drop silently. - Reasoning expander — assistant turns may carry an optional
reasoningfield 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=truerender 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
ANSWERcarries 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:
- 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.
Schema-aware SQL editor (v0.0.94)
The card add/edit modal (and the Card Library create/edit drawers) use a Monaco-based SQL editor with syntax highlighting, line numbers, and schema-aware autocomplete — tables after FROM/JOIN (schema-qualified) and columns from referenced tables (lazy-fetched and cached). The Browse columns click-insert still works via an imperative cursor-insert.
Calculated fields (v0.0.97)
The visual query builder supports calculated fields — a derived column defined by a SQL expression that becomes a selectable dimension or measure. The expression is validated server-side (a sqlglot AST walk: no subqueries or DML, allowlisted functions, columns must exist) and spliced into the generated SELECT. Definitions round-trip through card_config.calculated_fields. As of v0.1.7 the editor validates each expression interactively on blur — a green/red border, a ✓/✗ chip, inline error text (e.g. column 'bogus' is not available), and sample derived values from a 3-row read-only probe; saving blocks on a known-invalid expression instead of failing silently at preview.
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.
Chart conditional formatting (v0.0.96). Threshold-based coloring extends to charts and KPIs via card_config.chart_rules — { operator: gt|gte|lt|lte|eq|between, value, value2?, bg?, color? }. On single-series vertical and horizontal bars each bar colors by its value; on kpi / big_number the value text colors (chart_rules take precedence over kpi_thresholds). Multi-series is out of scope (ambiguous). Edited via a Conditional Formatting section in the card editor.
Reference / target lines (v0.0.96)
card_config.reference_lines draws horizontal markers at fixed y-values on bar / horizontal_bar / line / area / combo cards. A structured editor sets per-line y-value, label, color, and a dashed toggle (previously settable only via Advanced JSON).
Donut center total (v0.0.96)
Donut cards can fill the center hole with the sum of slices plus an optional caption (default "Total"), opt-in via card_config.show_center_total.
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 type | URL | Auth |
|---|---|---|
| Private | Same as the platform URL | Platform 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 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.
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 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.
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).
| Option | Card types | Notes |
|---|---|---|
color_scale | geo_scatter, choropleth | viridis, plasma, rdylgn, etc. |
number_format | kpi, big_number, metric_grid | Small DSL — "0.0%", "$#,##0". |
sort_by, sort_direction | table, pivot | Multi-column sort supported on pivot. |
stack | bar, area | group, stack, percent-stack. Percent-stacked tooltip surfaces both the raw and percentage value. |
chart_size | pie, donut | auto (default — responsive radius that fills the card) or sm/md/lg/xl fixed radii (md = the legacy 65px). Legacy hole_size/inner_radius are reinterpreted as a proportional ratio so the ring keeps its shape at any size. (v0.1.6) |
Responsive grid (v0.0.97)
In View mode the grid derives per-breakpoint layouts: the authored 24-column layout is kept on large/medium screens, and on narrow breakpoints (mobile/tablet) cards restack to a full-width single column in reading order. The reflow is presentational only — Edit mode keeps the authored layout at every breakpoint and the mobile reflow is never persisted.
Width buckets (v0.0.61+)
Card widths are stored as one of three semantic buckets rather than raw pixel widths:
| Bucket | size_x | Use |
|---|---|---|
small | 8 | KPIs, sparklines |
medium | 12 | Paired charts, half-width tables |
full | 24 | Tables, 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+)
| Direction | Endpoint | Trigger |
|---|---|---|
| Honeyframe → SVG | GET /api/dashboards/{id}/export-svg | Export → 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 → Honeyframe | POST /api/dashboards/import-from-metabase | List 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 → Honeyframe | POST /api/dashboards/import-from-figma-make | List 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).
Cross-filter (v0.0.97). Distinct from drill-down (which filters every card), cross-filter is opt-in and scoped: a clicked value drills only sibling cards that opted in via card_config.cross_filter_columns (the source card is excluded — correct cross-filter semantics). Set the opt-in columns via a Cross-filter on columns editor in the card builder.
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, expanded through v0.0.95) is org-scoped and end-user-driven. Save any well-shaped card from the editor as a reusable template; reuse it in any other dashboard in the same org. Storage: honeyframe.dashboard_card_templates with (template_id, org_id, owner_user_id, name, card_type, sql_query, card_config, size_x, size_y).
There are now two ways to reuse a template (v0.0.89):
- Stamp (
POST .../insert) — a one-time copy: the template's SQL / type / config are duplicated into a new card with no link back. Later edits to the template do not touch the stamped card. - Link (
POST .../link) — writes atemplate_idFK on the new card. Editing the template (PUT /card-templates/{id}) fans the template-managed fields (sql_query,card_type,card_config,description) out to every linked card; the response reportslinked_cards_updated. Layout fields (grid position, size, title, page) stay per-card. Editing a template-managed field directly on a linked card returns409with the template name (layout + title edits still go through). Deleting a template demotes linked cards back to plain stamps (ON DELETE SET NULL). A linked card shows an amber "Linked to template" badge in its header with Edit template and Unlink from template actions.
Card Library page (/card-library)
A Metabase-style grid page (v0.0.90) lists every template the caller's org can see, each tile carrying a live chart preview rendered with the same renderer as deployed cards (lazy-loaded via IntersectionObserver so a 50-template library doesn't fire 50 queries on mount), owner, last-edited, and a clickable "Used by N dashboards" popover. The primary action is Add to dashboard (pick a target dashboard, link the template in). A standalone detail page lives at /card-library/:templateId.
Library management features (v0.0.92–v0.0.95):
- Param defaults — a template whose SQL references dashboard params (
{{appointment_date_start}}) can stash default values so the preview renders instead of erroring on unbound binds. The preview endpoint merges caller params over these defaults (caller wins). Template-only — does not fan out to linked cards. Edited via a JSON editor in the edit drawer. - Tags — chip tags per template; click a chip to filter; the active-filter bar is URL-synced (
?tag=foo). - Folders — single-level, org-shared categories with a toolbar filter (All /
<folder>/ Uncategorized). - Visibility —
org(default) orprivate; private templates are visible only to their owner, with a 🔒 badge. - Versioning — each edit snapshots a version; a Diff viewer shows field-level before/after, and any version can be Restored.
- Bulk actions — multi-select to bulk-delete, bulk-add to a dashboard, or bulk-move to a folder.
- Pin / favorite and a sort control (recently updated / name / most used).
| Endpoint | Description |
|---|---|
GET /api/dashboards/card-templates | List templates (owner/used-by/updated-at/tags/folder/param-defaults). |
POST /api/dashboards/card-templates | Save the current card as a template. |
PUT /api/dashboards/card-templates/{id} | Update template; fans managed fields out to linked cards. |
DELETE /api/dashboards/card-templates/{id} | Remove (owner only). |
POST /api/dashboards/card-templates/{id}/insert | Stamp (one-time copy). |
POST /api/dashboards/card-templates/{id}/link | Link (live FK to template). |
POST /api/dashboards/cards/{cid}/unlink-template | Demote a linked card to a plain copy. |
POST /api/dashboards/card-templates/{id}/preview | Run the template SQL (merging param defaults) for a live preview. |
POST /api/dashboards/card-templates/{id}/duplicate | Copy a template (tags carried; owner = caller). |
GET /api/dashboards/card-templates/{id}/versions | Version history. |
GET /api/dashboards/card-templates/{id}/versions/{n} | Full snapshot for diff/restore. |
Dashboard templates (instance fan-out, v0.0.92–v0.0.93)
The card-template "1 → many" pattern also exists at the whole-dashboard level. Stamp a dashboard as a template, then fan it out into many parameterized instances — the headline case is one "multi-branch" layout fanned out across dozens of branch instances, each binding its own :branch_code.
- Stamp as template / Instantiate —
POST .../instantiateclones a template into a new instance. The instance carries its owntemplate_param_valuesJSONB; at render time the instance bindings slot between policy (lowest) and caller (highest), so a webapp filter bar still wins. Edit per-instance viaPUT /api/dashboards/{id}/template-params. - Bulk instantiate —
POST .../bulk-instantiatecreates up to 200 instances in one all-or-nothing transaction; each CSV row supplies a title and its param values. Opening the params modal auto-discovers:paramsfrom the template's card SQL and pre-fills empty rows. - Sync to instances —
POST .../sync-to-instancespropagates every template card change (SQL / type / config / description; adds and removes too) to every linked instance. Layout fields are never touched, so each instance keeps its own arrangement. A sync-preview (GET .../sync-preview) returns dry-run{added, updated, removed}counts; the UI shows a confirm modal before a fan-out mutation, since the blast radius can be large. - Browse page (
/dashboard-templates) — lists every dashboard with ≥1 linked instance. Header badges show "Template · N instances" and "Instance of #X"; a Sync → N button appears only on templates with instances.
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.
Project bundles (v0.0.98)
For promoting whole projects (not just dashboards), a Bundles tab in Project Settings provides a versioned lifecycle over the existing project export/import. Snapshot a source project as an immutable, numbered .hsproj.zip, then:
- Activate — clone a version into a brand-new project (the 1-master → many-instance templating path). Surfaces any
missing_connectorson the target. - Deploy / rollback — blue-green replace of an existing project: the bundle imports as a fresh project that takes over the target's slug and name while the prior contents are soft-archived (recoverable via project restore). The
project_idchanges; the stable slug handle does not. Atomic — a connector or import failure leaves the target untouched. - Push to a remote node — register a remote Honeyframe install (base URL + AES-GCM-encrypted PAT, never returned by the API) and push a stored bundle to its
/api/projects/importover HTTP. Reframes node promotion to the real test → prod boundary.
Endpoints live under /api/projects/{id}/bundles (create / list / download / activate), /api/bundles/{version}/deploy and /push, and /api/nodes (CRUD). All bundle mutations are org.admin. The SDK exposes Bundle.activate/deploy/push and client.register_node/list_nodes/delete_node.
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 | Stamp a card template into a dashboard (one-time copy). |
POST /api/dashboards/card-templates/{id}/link | Link a card template into a dashboard (live FK). |
PUT /api/dashboards/card-templates/{id} | Update a card template; fans managed fields to linked cards. |
POST /api/dashboards/card-templates/{id}/preview | Run template SQL for a live library preview. |
POST /api/dashboards/{id}/instantiate | Clone a dashboard template into a parameterized instance. |
POST /api/dashboards/{id}/bulk-instantiate | Create up to 200 instances in one transaction. |
POST /api/dashboards/{id}/sync-to-instances | Propagate template card changes to every linked instance. |
GET /api/dashboards/{id}/sync-preview | Dry-run {added, updated, removed} for a fan-out. |
PUT /api/dashboards/{id}/template-params | Set an instance's per-instance param bindings. |
GET /api/dashboards/template-library | List dashboards with ≥1 linked instance. |
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.