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_typeis one oftable,bar,line,pie,kpi,donut,area,stacked_bar,horizontal_bar,combo,pivot,textorsection_divider.card_configholds the chart settings:{xField, yField}for bar charts,{nameField, valueField}for pie and donut,{color_theme}for a KPI. The olderconfig=argument still works but is deprecated.grid_row=-1lets 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:
| Builder | Filter |
|---|---|
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
| Builder | Block |
|---|---|
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=Truelets the block set page filters by posting messages to the page;allow_cdn=Trueallows 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.
| Builder | Filter |
|---|---|
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.
Publishing and public links
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_days | Result |
|---|---|
30 (default) | Expires after 30 days. |
1 … 365 | Expires after that many days. |
0 | Never expires: an explicit opt-in. |
| anything else | ValueError (the server would silently make the link permanent). |
None | Never 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.