Skip to main content

Dashboards and webapps

A dashboard is a grid of SQL-backed cards. A webapp is a published, multi-page application whose blocks either embed dashboard cards or carry their own governed SQL and HTML. Both are built and shared entirely from code. For the concepts, see Dashboards and Webapps.

Dashboards​

dash = project.create_dashboard("Orders overview", description="Volume and revenue by store")

dash.add_card("Total orders", "kpi",
"SELECT COUNT(*) AS n FROM marts.fact_orders",
card_config={"color_theme": "ocean"}, size_x=6, size_y=2)

dash.add_card("Revenue by store", "bar",
"SELECT s.store_name, SUM(o.amount) AS revenue "
"FROM marts.fact_orders o JOIN marts.dim_store s USING (store_key) "
"GROUP BY s.store_name",
card_config={"xField": "store_name", "yField": "revenue"})

results = dash.execute_all()
print(dash.url)

project.create_dashboard(title, *, description=None, is_public=False, collection_id=None) creates a dashboard owned by the project. client.get_dashboard(dashboard_id) and project.get_dashboard(dashboard_id) load an existing one; client.list_dashboards(tab="all") and project.list_dashboards() list them (tab is my, shared, all or public).

Cards​

add_card(title, card_type, sql="", *, card_config=None, description=None,
grid_col=0, grid_row=-1, size_x=12, size_y=4, page_number=1)
  • card_type is one of table, bar, line, pie, kpi, donut, area, stacked_bar, horizontal_bar, combo, pivot, text or section_divider.
  • card_config holds the chart settings: {xField, yField} for bar charts, {nameField, valueField} for pie and donut, {color_theme} for a KPI. The older config= argument still works but is deprecated.
  • grid_row=-1 lets the server place the card.

Already have card definitions as dicts? dash.add_cards([...]) takes a list of {"title", "card_type", "sql_query", "card_config", …} dicts.

for card in dash.cards: # Card handles (call dash.reload() if stale)
print(card.card_id, card.card_type, card.title)

card = dash.cards[0]
card.execute() # {columns, rows, row_count, …}
card.update(title="Orders (all stores)", size_x=8)
card.delete()

Running cards, parameters and filters​

from honeyframeapi import filter_between, filter_in

res = dash.execute_all(
parameters={"region": "north"}, # binds {{region}} tokens in card SQL
filters=[filter_between("order_date", "2026-01-01", "2026-03-31"),
filter_in("store_key", ["1", "2", "3"])],
timeout=300,
)

execute_all runs every card and returns {results: {card_id: {...}}, …}. filters applies global filters to every card whose SQL references the filter column. Build them with:

BuilderFilter
filter_between(column, start, end)start <= column <= end
filter_compare(column, operator, value)operator is eq, gte or lte
filter_in(column, values)column IN (values)
dashboard_filter(column, *, operator="between", value=None, start=None, end=None, values=None, ftype=None)The general form; ftype (categorical, explicit, numeric_range, date_relative) coerces the bound value's type.

dash.execute_batch(card_ids, *, params=None, asset_id=None) (0.1.8+) runs a chosen subset of cards in one request, each exactly as Card.execute would. Its result is keyed by card id as strings (res[str(card_id)]); a failing card reports its own error without failing the batch.

Export​

dash.export_pdf("orders.pdf", paper_size="A4", orientation="landscape", timeout=120)
dash.export_pdf("orders.png", file_type="png", width=1600, scale=2) # 0.1.8+

The server renders the dashboard with a headless browser. paper_size is a4, a3, letter or screen_16_9; current_page_only=True with page_number exports one page. file_type, width, height (320–4096 px) and scale (0.5–3.0, PNG only) are 0.1.8+. When the server has no headless browser it answers 503, which the SDK raises as HoneyframeError; export from the browser instead.

Revisions, sharing and collections​

dash.revisions() # newest first
dash.create_revision("before redesign")
dash.diff_revisions(3, 5)
dash.restore_revision(3) # snapshots the current state first
dash.undo() # 409 when there is nothing to undo
dash.undo_card(dash.cards[0].card_id) # undo the last change to one card

share = dash.add_share(role="analyst", access_level="view") # or user_id=; exactly one
dash.shares()
dash.update_share(share["share_id"], "edit")
dash.remove_share(share["share_id"])

client.create_collection("Finance")
dash.move_to_collection(collection_id) # an id from client.list_collections(); None uncategorizes

client.list_collections(), update_collection and delete_collection manage collections; deleting one moves its dashboards back to uncategorized. Sharing calls are owner-only.

Card templates​

A card template is an organization-wide reusable card. Stamp it onto a dashboard as a one-off copy, or as a linked card that follows later template edits.

tpl = client.create_card_template(
"Revenue KPI", "kpi",
sql="SELECT SUM(amount) AS revenue FROM marts.fact_orders",
card_config={"color_theme": "ocean"},
)
tpl.link_into(dash.dashboard_id) # follows template edits
tpl.insert_into(dash.dashboard_id) # a one-off copy
tpl.update(sql_query="SELECT SUM(net_amount) AS revenue FROM marts.fact_orders")
tpl.preview() # run the template's SQL (needs a project on the client)

client.list_card_templates()

update fans changes to card_type, sql_query, card_config and description out to every linked card and returns how many it updated. Card.unlink_template() detaches one card. versions(), get_version(n), restore_version(n) and duplicate() manage the template's history and copies.

Webapps​

Webapps are project-scoped: set project_id on the client before creating one.

Build a webapp from builders​

from honeyframeapi import (
page, nav_item, card_ref, code_card, html_block,
filters, date_range_filter, entity_filter, chrome,
)

app = client.create_webapp(
"store-performance", "Store performance",
pages=[page(
"home", "Home",
blocks=[
card_ref(dash.dashboard_id, dash.cards[0].card_id, w=12, h=4),
code_card(
"SELECT store_name, SUM(amount) AS revenue FROM marts.fact_orders "
"JOIN marts.dim_store USING (store_key) "
"WHERE order_date BETWEEN :period_start AND :period_end "
"GROUP BY store_name",
"bar", x=12, w=12, h=4, title="Revenue by store"),
html_block("<p>Figures refresh nightly.</p>", w=24, h=2),
],
filters=filters(
period=date_range_filter(locale="en", timezone="UTC"),
store=entity_filter("store_codes", options_sql=(
"SELECT store_code AS value, store_name AS label FROM marts.dim_store")),
),
)],
nav=[nav_item("home", "Home", icon="gauge")],
chrome=chrome(show_sidebar_search=True, show_clock=True, timezone="UTC"),
publish=True,
)
print(app.url)

create_webapp(asset_key, title, *, pages=None, nav=None, filters=None, chrome=None, description="", theme=None, roles=None, is_public=False, icon="", publish=False) creates the webapp from builder output. It creates a draft unless publish=True: the webapp viewer (/apps/<asset_key>) serves published webapps only. Use a fresh asset_key, or edit an existing webapp with the methods below. client.publish_webapp(asset_key, title, *, config=...) is the lower-level form that takes the raw config dict.

client.webapp_capabilities() returns what the webapp engine supports on your installation (block kinds and their schemas, card types, size and SQL limits), so a script can check before it builds.

Blocks​

BuilderBlock
card_ref(dashboard_id, card_id, *, x=0, y=0, w=12, h=4, overrides=None)Embeds an existing dashboard card.
code_card(sql, card_type, *, x, y, w, h, title, card_config, display_options)A governed, read-only SQL query rendered as a chart, run on the server.
html_block(html, *, mode="sanitized", ...)Freeform HTML/CSS/JS (see below).
form_card(form_key, embed_token, *, title=None, ...)Embeds an existing published form so the webapp can collect submissions.
form(form_id, fields, *, title=None, success_message=None, ...) with field(name, label, type="text", ...)A form declared inline; submissions are stored per webapp.
leaderboard_card(*, board=None, limit=None, title=None, ...)The webapp's shared leaderboard.
banner(branches_sql, *, title=None, severity="warning", locale="id", ...)A full-width attention banner, one chip per row of the query (0.1.8+).

Layout arguments x, y, w, h place a block on the page grid.

HTML blocks. mode="sanitized" strips scripts and suits rich text. mode="sandboxed" runs arbitrary JavaScript in an isolated iframe with no access to the viewer's session and no outbound network. In a sandboxed block:

  • data_sql= is a read-only query the server runs and posts into the iframe, so your script renders real data without any API access of its own;
  • emits_filters=True lets the block set page filters by posting messages to the page;
  • allow_cdn=True allows a fixed list of library CDNs;
  • persist_key= gives the block a stable slot for state saved in the viewer's browser.

public_hidden=True keeps a block off anonymous share-link views. Each block is limited to 100,000 characters.

Pages, navigation and filters​

page(key, title, blocks, *, subtitle=None, footnote=None, icon=None, filters=None,
header_meta=None, header_alert=None, header_alert_severity=None,
header_meta_sql=None, header_alert_sql=None, anchors=None)
nav_item(page_key, label, *, icon="circle", section=None)

anchors is a list of page_anchor(label, block_index) pills that scroll to a block (0.1.8+); an index outside blocks raises ValueError.

Filter builders produce the filter slots; assemble them with filters(**slots) and attach them to a page or to the whole webapp (filters= on create_webapp, or app.set_filters(...)). Card and block SQL binds the chosen values by parameter name.

BuilderFilter
date_range_filter(*, start_param="period_start", end_param="period_end", timezone="Asia/Jakarta", presets=None, label_param=None, locale="id")A preset date range; binds :period_start / :period_end.
branch_filter(param, *, mode="multi", options=None, options_sql=None, empty_value=None, chips=False)A dropdown; static options and/or an options_sql query returning value, label.
entity_filter(...)The same as branch_filter, under a neutral name (0.1.8+).
pill_multi_filter(param, options, *, label=None, mode="multi", all_label=None, empty_value=None, chips=False)An inline row of pills over a fixed list (0.1.8+).
metric_search_filter(*, label=None, placeholder=None)A search box that finds a card; binds nothing (0.1.8+).

The timezone and locale defaults are kept for compatibility with existing apps: pass your own time zone, and locale="en" for English default labels (locale is 0.1.8+). chips=True needs mode="multi".

Theme and chrome​

from honeyframeapi import theme, chrome

app.set_theme(theme(primary_color="#0E7C66", font_scale=1.1)) # full replace
app.update_theme(theme(border_radius="lg")) # merge, validated by the server
app.set_chrome(chrome(show_theme_toggle=True, show_print_button=True)) # 0.1.8+

chrome(...) turns header and rail affordances on; every one is off unless you pass it, except show_report_cart, which is on by default. A flag whose content comes from a query must come with that query (an alert pill needs alert_pill_sql or alert_pill_text; notifications need notifications_sql; the assistant drawer needs assistant={"agent_id": ...}), and content passed without its flag is refused too. chrome is 0.1.8+.

Edit, publish and version​

app = client.get_webapp("store-performance")

app.add_page(page("detail", "Detail", [code_card("SELECT 1 AS n", "kpi")]),
nav=nav_item("detail", "Detail"), publish=True)
app.set_nav([nav_item("home", "Home"), nav_item("detail", "Detail")])
app.remove_page("detail")
app.update(title="Store performance (beta)", change_note="rename")
app.publish()
app.unpublish()

app.versions() # every save, newest first
app.diff_versions(7, 8)
app.rollback(7) # re-applied as a new version; org admin

The page and nav helpers read the stored config, change it and save it for you; update(config=...) overwrites the whole config. Editing does not republish: call publish() to push changes live. If a save succeeds but the SDK cannot read the webapp back afterwards, it warns, sets app.last_warning, and keeps the (stale) local copy instead of reporting a failed save.

app.ai_edit(prompt) asks the platform's assistant for a complete new config and returns it with a change summary, without saving; apply=True saves it as a new version tagged AI: <prompt>. app.ai_theme(prompt) proposes and applies a theme change.

Read data from a published webapp (0.1.8+)​

res = app.execute_block("home", 1, {"period_start": "2026-01-01", "period_end": "2026-01-31"})
if res["error"]:
raise RuntimeError(res["error"])
print(res["columns"], res["row_count"], res["truncated"])

cat = app.catalog(page_key="home") # which datasets back the app, without SQL or rows

execute_block(page_key, block_idx, params=None) runs the SQL a block already holds (the index is the block's position on the page); params only binds values. Errors come back in the result's error key rather than as an exception, and truncated is true at the 5,000-row cap. A read-only token can make this call.

Dashboards and webapps can be shared with people who have no account through token-gated public links. Anonymous viewers get a masked view.

link = app.create_public_link(label="board pack", expires_days=30)
print(link["token"]) # shown ONCE — store it
print(app.public_url(link["token"])) # the anonymous URL

for l in app.public_links(): # metadata only, never the token
print(l["label"], l["status"], l["view_count"])
app.revoke_public_link(link["id"])

create_public_link(*, label=None, expires_days=30) on Webapp and Dashboard:

expires_daysResult
30 (default)Expires after 30 days.
1 … 365Expires after that many days.
0Never expires: an explicit opt-in.
anything elseValueError (the server would silently make the link permanent).
NoneNever expires, deprecated: pass 0.

The 30-day default and the validation are 0.1.8+; 0.1.7 links never expire unless you pass expires_days. If public webapps are served from a different host than the API, pass it: app.public_url(token, host="https://apps.your-domain.com").

Published assets and data-API keys​

Webapps, API services, data APIs, forms and agents are all published assets. The generic handle is PublishedAsset:

client.list_assets(asset_type="data_api", status="published")
asset = client.create_asset("data_api", "orders-feed", "Orders feed",
config=data_api_config) # the asset type's configuration dict
asset.publish()
key = asset.create_api_key(name="partner") # ApiKey; key.key is the dk_… secret, shown once
asset.list_api_keys()
asset.revoke_api_key(key.id)

asset.versions()
asset.rollback(3)

asset.update(*, change_note="", **fields) sends the fields you pass (plus change_note) as a PUT of the asset. For webapps and API services, prefer their own editing methods, which read the stored config, change it and save it.

Key defaults and sensitivity tiers are described under Authentication → Data-API keys.


Verified against honeyframeapi 0.1.8.