Back to Cube

Events & actions

docs-mintlify/embedding/iframe/events.mdx

1.7.3221.2 KB
Original Source
<Note>

Available on Premium and above plans.

</Note>

Embedded Cube surfaces communicate with your host page over the browser postMessage API, in both directions:

  • Events (cube:event:*) travel embed → host. Subscribe to them to learn how a viewer interacts with the embed — when it loads, what they view, download, drill into, search for with AI, and any errors they hit. Feed them into your own analytics / event bus.
  • Actions (cube:action:*) travel host → embed. Send them to drive the embed from your app — switch the color scheme, set a filter, navigate, or refresh the data.

No SDK is required — it's plain window.postMessage and a message listener.

Message envelope

Every message — in either direction — is a single object with the same shape:

ts
{
  source: "cube-embed",              // discriminator — always this string
  direction: "event" | "action",     // "event" = embed→host, "action" = host→embed
  type: string,                      // e.g. "cube:event:download" / "cube:action:set-filter"
  payload: object,                   // shape depends on `type` (see catalogs below)
  timestamp: number,                 // epoch milliseconds
  surface?: "dashboard" | "app" | "chat" // always on events; never on actions
}
FieldDescription
sourceAlways "cube-embed". Check this first to tell Cube messages apart from other postMessage traffic on the page (browser extensions, other libraries, your own app).
direction"event" for messages emitted by the embed, "action" for messages you send into it.
typeThe event or action name (see the catalogs below).
payloadEvent/action-specific data.
timestampWhen the message was created, in epoch milliseconds.
surfaceWhich embedded surface the message relates to. Always present on events; never sent on actions — so event listeners never need to handle a missing surface.

Listening to events

Attach a single message listener to window. Always validate event.origin against your tenant's origin and check data.source === "cube-embed" before trusting a message.

js
const CUBE_ORIGIN = "https://your-tenant.cubecloud.dev";

window.addEventListener("message", (event) => {
  // 1. Only trust messages from your Cube tenant.
  if (event.origin !== CUBE_ORIGIN) return;

  const data = event.data;

  // 2. Only handle Cube embed events.
  if (!data || data.source !== "cube-embed" || data.direction !== "event") return;

  // 3. Dispatch on the event type.
  switch (data.type) {
    case "cube:event:ready":
      console.log("Embed ready", data.payload.embedTenant, data.payload.deploymentId);
      break;
    case "cube:event:download":
      myAnalytics.track("embed_download", data.payload);
      break;
    case "cube:event:ai-query":
      myAnalytics.track("embed_ai_query", { query: data.payload.query });
      break;
    case "cube:event:error":
      console.error("Embed error", data.payload.message);
      break;
    default:
      // ready, view, navigate, dashboard-loaded, drilldown, …
      myAnalytics.track(data.type, { surface: data.surface, ...data.payload });
  }
});

Event catalog

EventFires whenSurfaces
cube:event:readyThe embed has authenticated and mounted (the handshake)all
cube:event:viewA surface is viewed, on load and on each in-embed navigationall
cube:event:navigateThe viewer navigates within the embedall
cube:event:dashboard-loadedAll widgets on a dashboard have rendereddashboard
cube:event:downloadThe viewer exports data or an imagedashboard
cube:event:drilldownThe viewer drills into a measuredashboard
cube:event:ai-queryThe viewer runs an AI / natural-language queryall
cube:event:session-expiringA signed session is close to expiring (~30 min out)all
cube:event:session-expiredA signed session has expiredall
cube:event:errorThe embed surfaces an errorall
<Note> Every event payload is also delivered with the envelope's `surface` field, so you can always tell which surface (`dashboard`, `app`, or `chat`) it came from — including AI queries, which report `app` when run inside the embedded app and `chat` on the standalone chat surface. </Note>

cube:event:ready {#cube-event-ready}

Emitted once per session, as soon as the embed authenticates and mounts. The handshake — the first event you receive, and the moment to record an "embed opened".

FieldTypeDescription
embedTenantstring | nullThe embed tenant the iframe resolved to, when known.
deploymentIdnumber | nullThe deployment the embed is bound to, when known.
mode"signed" | "private"How the viewer was authenticated.
surface"dashboard" | "app" | "chat"The surface that mounted.
publicIdstring (optional)The dashboard's public id, for the dashboard surface.
json
{
  "embedTenant": "acme",
  "deploymentId": 42,
  "mode": "signed",
  "surface": "dashboard",
  "publicId": "a1b2c3d4"
}

cube:event:view {#cube-event-view}

Emitted when a surface is viewed — on the initial load and again whenever the viewer navigates within the embed.

FieldTypeDescription
surface"dashboard" | "app" | "chat"The surface viewed.
pathstringThe in-embed route path that was viewed.
publicIdstring (optional)The dashboard's public id, when applicable.
titlestring (optional)Human-readable title of the surface, when available.
json
{
  "surface": "app",
  "path": "/embed/d/42/app/workbook/130"
}

cube:event:navigate {#cube-event-navigate}

Emitted when the viewer navigates within the embed (a route change). Use it to mirror the embed's location in your own router or analytics.

FieldTypeDescription
pathstringThe new path.
previousPathstring (optional)The path navigated away from.
json
{
  "path": "/embed/d/42/app/workbook/130",
  "previousPath": "/embed/d/42/app"
}

cube:event:dashboard-loaded {#cube-event-dashboard-loaded}

Emitted when a dashboard has finished rendering all of its widgets — the "fully painted" signal (distinct from ready, which fires at mount, before data loads).

FieldTypeDescription
publicIdstring (optional)The dashboard's public id.
widgetCountnumber (optional)Number of widgets on the dashboard.
loadDurationMsnumber (optional)Milliseconds from mount to all widgets loaded, when measurable.
json
{
  "publicId": "a1b2c3d4",
  "widgetCount": 6
}

cube:event:download {#cube-event-download}

Emitted when a viewer exports something — a widget's data as CSV, or a widget as a PNG or PDF image. Reports that an export happened and its shape — never the exported rows themselves.

FieldTypeDescription
format"csv" | "xlsx" | "png" | "pdf"The file format produced.
target"widget" | "dashboard"Whether a single widget or the whole dashboard was exported.
widgetIdstring (optional)Id of the source widget, when target is "widget".
titlestring (optional)Title of the exported widget / dashboard.
rowCountnumber (optional)Rows exported, for data exports (csv / xlsx).
json
{
  "format": "csv",
  "target": "widget",
  "widgetId": "37",
  "title": "Revenue by month",
  "rowCount": 128
}
<Note> A dashboard widget's download actions (CSV, PNG, PDF) only appear when the embed URL includes `allowExport=true` (see [Dashboards → Allow chart export](/embedding/iframe/dashboards#allow-csv-export)). The event fires when a viewer uses one of them. </Note>

cube:event:drilldown {#cube-event-drilldown}

Emitted when a viewer drills into a measure (clicks a chart mark or table cell to see its detail rows).

FieldTypeDescription
memberstringThe fully-qualified measure that was drilled into.
valueunknown (optional)The clicked value, when the click carried one.
widgetIdstring (optional)Id of the originating widget.
json
{
  "member": "orders.count",
  "value": "completed",
  "widgetId": "37"
}

cube:event:ai-query {#cube-event-ai-query}

Emitted around an AI / natural-language query — capturing what the viewer asked and the lifecycle stage. Fires wherever AI chat is used: the standalone chat surface, the dashboard agent, and the embedded app.

FieldTypeDescription
querystringThe natural-language query the viewer submitted.
status"submitted" | "completed" | "error"Lifecycle stage of the query.
chatIdstring (optional)The chat/session id, when applicable.
agentIdstring (optional)The agent that answered, when applicable.
json
{
  "query": "top 10 customers by revenue this quarter",
  "status": "submitted",
  "agentId": "1"
}

cube:event:session-expiring {#cube-event-session-expiring}

Emitted once per signed embedding session, ~30 minutes before it stops working. This is the moment to mint a replacement session and push it in with cube:action:set-session — see Keeping a signed session alive below.

FieldTypeDescription
expiresAtnumberEpoch ms when the session actually stops working — earlier than the token's nominal 24-hour expiry, see Session lifecycle.
expiresInMsnumberMilliseconds from this event to expiresAt. 0 if already past it.
json
{
  "expiresAt": 1735689600000,
  "expiresInMs": 1800000
}
<Note> Only fires for signed embedding sessions — a private-embedding iframe has no expiring session to renew. It's a best-effort timer inside the iframe: a hidden or suspended tab can throttle it, so it may arrive late (immediately on wake) or after `cube:event:session-expired`. </Note>

cube:event:session-expired {#cube-event-session-expired}

Emitted when a signed session has actually lapsed. The embed can't recover on its own — there's no refresh token or re-exchange endpoint — so nothing happens until you push a fresh session in with cube:action:set-session.

FieldTypeDescription
expiredAtnumberEpoch ms when the session was observed to have lapsed.
json
{
  "expiredAt": 1735689600000
}

cube:event:error {#cube-event-error}

Emitted when the embed surfaces an error (a render error, a query failure, an auth/session problem). fatal distinguishes an error that took the whole surface down from a recoverable one.

FieldTypeDescription
messagestringHuman-readable message.
namestring (optional)Error name/class, e.g. "TypeError".
contextstring (optional)Where it originated, e.g. "embed-render", "session-renewal".
fatalboolean (optional)true when the error took down the whole surface.
json
{
  "message": "Failed to load data",
  "context": "embed-render",
  "fatal": true
}

Sending actions

Send actions into the embed by posting a message to the iframe's contentWindow. Always target your tenant's origin (not "*") so the message can't leak to another document if the iframe navigates away.

js
const iframe = document.querySelector("iframe#cube");
const CUBE_ORIGIN = "https://your-tenant.cubecloud.dev";

function sendAction(type, payload = {}) {
  iframe.contentWindow.postMessage(
    {
      source: "cube-embed",
      direction: "action",
      type,
      payload,
      timestamp: Date.now(),
    },
    CUBE_ORIGIN
  );
}

// Examples
sendAction("cube:action:set-color-scheme", { scheme: "dark" });
sendAction("cube:action:set-filter", {
  filterUrlParameter: 'f_orders.status={"value":"completed"}',
});
sendAction("cube:action:refresh");

Action catalog

ActionEffectPayload
cube:action:set-color-schemeSwitch light / dark / auto{ scheme }
cube:action:set-themeApply a brand theme (colors, fonts)embedTheme object
cube:action:set-localeSwitch the UI language{ locale }
cube:action:set-timezoneSwitch the query time zone{ timezone }
cube:action:set-filterPush a filter into a dashboard{ filterUrlParameter }
cube:action:navigateNavigate the embed to a path{ path }
cube:action:refreshRe-run the embed's queriesnone
cube:action:set-sessionSwap in a fresh signed session, in place (no iframe reload){ sessionId }

cube:action:set-color-scheme {#cube-action-set-color-scheme}

Switch the embed's color scheme at runtime.

FieldTypeDescription
scheme"light" | "dark" | "auto""auto" follows the viewer's OS preference.
js
sendAction("cube:action:set-color-scheme", { scheme: "dark" });

cube:action:set-theme {#cube-action-set-theme}

Apply a brand theme (colors, fonts) to the embed at runtime. The payload is an embedTheme object — the same shape the Generate Session API accepts. Common fields are primaryColor, borderRadius, and font; see App customization for the full list.

js
sendAction("cube:action:set-theme", {
  primaryColor: "#7c5cff",
  borderRadius: 8,
});

cube:action:set-locale {#cube-action-set-locale}

Switch the embed's UI language. Accepts a full code (es-ES), a short code (es), or a regional variant. See Localization for the list of supported languages and the other ways to set the language.

FieldTypeDescription
localestringThe locale to switch to.
js
sendAction("cube:action:set-locale", { locale: "es" });

cube:action:set-timezone {#cube-action-set-timezone}

Switch the time zone the embed's queries run in — which day a row falls into, and what today means. Takes precedence over the ?timezone= URL parameter and the account default. See Time zones for the other ways to set it.

FieldTypeDescription
timezonestringAn IANA time zone name, e.g. Asia/Tokyo. Bare UTC offsets are ignored.
js
sendAction("cube:action:set-timezone", { timezone: "Asia/Tokyo" });

cube:action:set-filter {#cube-action-set-filter}

Push a filter into a dashboard. The filterUrlParameter is the same f_<semantic_view>.<dimension>=<JSON> form used to pre-set filters via URL, so you can capture a viewer's filters and restore them later.

FieldTypeDescription
filterUrlParameterstringFilter(s) in URL-query form, e.g. f_orders.status={"value":"completed"}.
js
sendAction("cube:action:set-filter", {
  filterUrlParameter: 'f_orders.status={"value":"completed"}',
});

cube:action:navigate {#cube-action-navigate}

Navigate the embed to an in-embed path.

FieldTypeDescription
pathstringThe in-embed path to navigate to.
js
sendAction("cube:action:navigate", { path: "/embed/d/42/app/workbook/130" });

cube:action:refresh {#cube-action-refresh}

Re-run the embed's queries and refresh its data. No payload.

js
sendAction("cube:action:refresh");

cube:action:set-session {#cube-action-set-session}

Hand the embed a fresh signed embedding session id, replacing the one it's running on without reloading the iframe. Sent before the current session lapses, the swap is invisible — nothing unmounts, so the viewer keeps their place, filters, and any unsaved editing state. Sent after it has already lapsed, it still recovers the embed without a reload, but the surface has been torn down by then and transient state is gone.

FieldTypeDescription
sessionIdstringA single-use session id from the Generate Session API. Expires 5 minutes after it's minted, so mint it at the moment you send it rather than ahead of time.
js
sendAction("cube:action:set-session", { sessionId: newSessionId });

A rejected id (unknown, already redeemed, or expired) doesn't tear down a working embed. It's reported via cube:event:error with context: "session-renewal" and name: "EmbedSessionExchangeError", so you can filter for it and retry.

Keeping a signed session alive

A signed embed's token is usable for about 23 hours (see Session lifecycle) and can't refresh itself — left alone, a tab open that long drops to a "Session expired" message. Renew it in place instead, using the events and action above:

js
let renewing = false;

window.addEventListener("message", (event) => {
  if (event.origin !== CUBE_ORIGIN) return;
  const data = event.data;
  if (!data || data.source !== "cube-embed" || data.direction !== "event") return;

  if (data.type === "cube:event:session-expiring" || data.type === "cube:event:session-expired") {
    // Both events can fire for one session — renew only once.
    if (renewing) return;
    renewing = true;

    fetch("/api/cube-embed-session", { method: "POST" }) // your backend, calling Generate Session
      .then((r) => r.json())
      .then(({ sessionId }) => sendAction("cube:action:set-session", { sessionId }))
      .catch((error) => console.error("Session renewal failed", error))
      .finally(() => {
        renewing = false;
      });
  }
});

Mint the replacement session on your own backend — Generate Session needs an API key that must never reach the browser. Each id is single-use and expires 5 minutes after minting, so mint it in response to the event rather than caching one ahead of time.

Surfaces

Events come from one of three customer-facing surfaces, reported in the envelope's surface field:

Security

  • Always validate event.origin against your tenant's origin in your message listener, and check data.source === "cube-embed". Never act on a message that fails either check.
  • Target your tenant's origin when sending actions (iframe.contentWindow.postMessage(msg, CUBE_ORIGIN)), not "*", so an action can't be delivered to an unexpected document.

Complete example

A minimal host page that loads a signed dashboard embed, logs every event, and exposes buttons to drive it. Generate the session on your backend with the Generate Session API — see Signed embedding for the full flow.

html
<!doctype html>
<html>
  <body>
    <button id="dark">Dark mode</button>
    <button id="refresh">Refresh</button>

    <iframe
      id="cube"
      title="Dashboard"
      src="https://your-tenant.cubecloud.dev/embed/dashboard/YOUR_DASHBOARD_PUBLIC_ID?session=YOUR_SESSION_ID"
      width="100%"
      height="800"
    ></iframe>

    <script>
      const CUBE_ORIGIN = "https://your-tenant.cubecloud.dev";
      const iframe = document.getElementById("cube");

      // Receive events (embed → host)
      window.addEventListener("message", (event) => {
        if (event.origin !== CUBE_ORIGIN) return;
        const data = event.data;
        if (!data || data.source !== "cube-embed" || data.direction !== "event") return;

        console.log(`[${data.surface}] ${data.type}`, data.payload);
        // → forward to your own analytics / event bus here
      });

      // Send actions (host → embed)
      function sendAction(type, payload = {}) {
        iframe.contentWindow.postMessage(
          { source: "cube-embed", direction: "action", type, payload, timestamp: Date.now() },
          CUBE_ORIGIN
        );
      }

      document.getElementById("dark").onclick = () =>
        sendAction("cube:action:set-color-scheme", { scheme: "dark" });
      document.getElementById("refresh").onclick = () =>
        sendAction("cube:action:refresh");
    </script>
  </body>
</html>