skillZs
★ LIVE SKILL TAGS ★
>>> LIVE SKILLS INDEX <<<
* OPEN SOURCE *
NO LOGIN, NO TRACKING
※ REAL INSTALL DATA ※
← back to all skills
stahura/domo-ai-vibe-rules164 installs

jsapi-filters

Apply runtime filters to embedded Domo dashboards/cards from the host page via the JS API (MessagePort). Covers dynamic filtering, drill events, filter change listeners, iframe resize, pfilter URL params, overrideFilters, and App Studio appData. Use for any client-side Domo embed interaction. Not for server-side programmatic filters (use programmatic-filters).

How do I install this agent skill?

npx skills add https://github.com/stahura/domo-ai-vibe-rules --skill jsapi-filters
view source ↗

Is this agent skill safe to install?

  • Gen Agent Trust Hubwarn

    The skill provides code for the Domo JS API that lacks critical origin validation for cross-document messaging. This creates a vulnerability where a malicious site could intercept communication ports or spoof Domo messages to influence the application state.

  • Socketpass

    No alerts

  • Snykpass

    Risk: LOW · No issues

  • ZeroLeakspass

    Score: 93/100 · 2 sections analyzed

What does this agent skill do?

Domo Client-Side Filters (JS API)

Dynamically filter embedded Domo content from your host page after the embed loads. Unlike server-side programmatic filters (baked into the embed token), client-side filters update instantly without re-fetching tokens.

Use for: UI-driven filtering (dropdowns, date pickers), drill-down responses, syncing filters across multiple embeds, passing context to App Studio apps. For server-side filters, see programmatic-filters.

How It Works

Domo iframes communicate with the host page via MessagePort (JSON-RPC 2.0):

  1. Domo iframe posts a message event with a MessagePort and referenceId
  2. Host page captures the port and listens for messages
  3. Host sends RPC calls (filters) through the port; Domo sends events (drills, filter changes) back

Setup

Step 1: Initialize the JS API Listener

Set up a global listener to capture each iframe's MessagePort handshake.

// lib/jsapi.ts

const ports: Record<string, MessagePort> = {};

export interface DomoRpcMessage extends MessageEvent {
  data: {
    referenceId?: string;
    method?: string;
    params?: any;
    result?: any;
    error?: any;
    id?: string;
    jsonrpc?: string;
  };
}

export const initializeJsApi = () => {
  if (typeof window === 'undefined') return;

  const handleMessage = (e: MessageEvent) => {
    if (!e.ports || !e.ports[0]) return;

    const referenceId = e.data?.referenceId;
    if (!referenceId) return;

    const port = e.ports[0];
    port.start();

    // Store the port for sending commands later
    ports[referenceId] = port;

    // Listen for events from this iframe
    port.onmessage = (event: MessageEvent) => {
      const data = event.data;
      if (!data) return;

      if (data.method) {
        handleRpcMethod(referenceId, data.method, data.params);
      }

      if (data.error) {
        console.error(`[JsApi] RPC error (ref: ${referenceId}):`, data.error);
      }
    };
  };

  window.addEventListener('message', handleMessage);

  return () => {
    window.removeEventListener('message', handleMessage);
  };
};

Step 2: Mount the Initializer Early

Mount before any Domo iframe loads (app root). React/Next.js:

// components/DomoJsApiInitializer.tsx
'use client'

import { useEffect } from 'react'
import { initializeJsApi } from '@/lib/jsapi'

export default function DomoJsApiInitializer() {
  useEffect(() => {
    const cleanup = initializeJsApi()
    return cleanup
  }, [])

  return null
}

Then include it in your root layout:

// app/layout.tsx
import DomoJsApiInitializer from './components/DomoJsApiInitializer'

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <DomoJsApiInitializer />
        {children}
      </body>
    </html>
  )
}

Step 3: Configure Embed Authorized Domains

Add your host domain to Admin > Network Security > Embed Authorized Domains in Domo. Without this, the iframe won't send the handshake message event. For custom Domo apps, also add to Custom Apps Authorized Domains.

Step 4: Use Embed Tokens (Not Public URLs)

The JS API only works with authenticated embeds (token-based POST flow). Public embed URLs don't initiate the MessagePort handshake. See programmatic-filters for the token flow.


Applying Filters

Send /v1/filters/apply through the stored MessagePort.

Filter Structure

Client-side filters use operand (not operator like server-side filters — this is the most common bug):

interface ClientFilter {
  column: string;
  operand: string;    // 'IN', 'NOT_IN', 'EQUALS', 'BETWEEN', 'CONTAINS', etc.
  values: any[];
  dataType?: string;      // 'STRING' | 'NUMBER' | 'DATE' — disambiguates values
  dataSourceId?: string;  // target a specific dataset in multi-dataset embeds
}

applyFilters Function

export const applyFilters = (
  filters: Array<{ column: string; operand: string; values: any[] }> = []
) => {
  Object.values(ports).forEach(port => {
    port.postMessage({
      id: `filter-${Date.now()}`,
      jsonrpc: '2.0',
      method: '/v1/filters/apply',
      params: {
        filters
      }
    });
  });
};

This broadcasts the filters to all connected Domo iframes. To target a specific iframe, use the referenceId:

export const applyFiltersToEmbed = (
  referenceId: string,
  filters: Array<{ column: string; operand: string; values: any[] }> = []
) => {
  const port = ports[referenceId];
  if (!port) {
    console.warn(`[JsApi] No port found for referenceId: ${referenceId}`);
    return;
  }

  port.postMessage({
    id: `filter-${Date.now()}`,
    jsonrpc: '2.0',
    method: '/v1/filters/apply',
    params: { filters }
  });
};

Examples

Filter by a single value:

applyFilters([
  { column: 'Region', operand: 'IN', values: ['West'] }
]);

Multiple filters (ANDed together):

applyFilters([
  { column: 'Region', operand: 'IN', values: ['West', 'East'] },
  { column: 'Year', operand: 'EQUALS', values: [2025] }
]);

Clear all filters:

applyFilters([]);

React Example — Filter Dropdown

'use client'

import { useState } from 'react'
import { applyFilters } from '@/lib/jsapi'

const regions = ['All', 'West', 'East', 'North', 'South']

export default function RegionFilter() {
  const [selected, setSelected] = useState('All')

  const handleChange = (region: string) => {
    setSelected(region)

    if (region === 'All') {
      applyFilters([])
    } else {
      applyFilters([
        { column: 'Region', operand: 'IN', values: [region] }
      ])
    }
  }

  return (
    <select value={selected} onChange={(e) => handleChange(e.target.value)}>
      {regions.map(r => (
        <option key={r} value={r}>{r}</option>
      ))}
    </select>
  )
}

Handling Events from Domo

Domo sends events back through the MessagePort. Use a subscribe/unsubscribe pattern for clean listener management:

// Event subscription pattern
type EventCallback = (referenceId: string, params: any) => void;
const listeners: Record<string, Set<EventCallback>> = {};

export function on(event: string, callback: EventCallback): () => void {
  if (!listeners[event]) listeners[event] = new Set();
  listeners[event].add(callback);
  return () => { listeners[event].delete(callback); };
}

Then in your handleRpcMethod, dispatch to registered listeners. Handle them in a central switch:

function handleRpcMethod(referenceId: string, method: string, params: any) {
  switch (method) {
    case '/v1/onDrill':
      handleDrill(referenceId, params);
      break;
    case '/v1/onFiltersChange':
      handleFiltersChange(referenceId, params);
      break;
    case '/v1/onAppReady':
      handleAppReady(referenceId, params);
      break;
    case '/v1/onAppData':
      handleAppData(referenceId, params);
      break;
    case '/v1/onFrameSizeChange':
      handleFrameResize(referenceId, params);
      break;
    default:
      break;
  }
}

/v1/onDrill — Drill Events

Fired when a user clicks a drillable element. Includes filters representing the drill path:

function handleDrill(referenceId: string, params: any) {
  const drillFilters = params?.filters;
  // drillFilters: [{ column: 'Region', operand: 'IN', values: ['West'] }]

  // Example: apply the drill filters to another embed on the page
  applyFiltersToEmbed('other-embed-ref', drillFilters);

  // Example: navigate to a detail page
  router.push(`/details?region=${drillFilters[0]?.values[0]}`);
}

/v1/onFiltersChange — Filter Change Events

Fired when filters change inside the embed (user interaction, card-to-card, or programmatic). Use to sync host UI:

function handleFiltersChange(referenceId: string, params: any) {
  const currentFilters = params?.filters;
  // currentFilters: [{ column: 'Region', operand: 'IN', values: ['West'] }]

  // Example: update your app's filter UI to reflect what's selected in Domo
  setActiveFilters(currentFilters);

  // Example: sync filters to another embed
  applyFiltersToEmbed('second-dashboard-ref', currentFilters);
}

/v1/onAppReady — App Studio Ready

Fired when an App Studio app finishes loading. Critical with overrideFilters=true — send filters only after this event:

function handleAppReady(referenceId: string, params: any) {
  // Now safe to send filters and appData
  applyFiltersToEmbed(referenceId, initialFilters);
  sendAppData(referenceId, { customerId: '12345' });
}

/v1/onAppData — App Data from Domo

Fired when an App Studio app sends data back to the host:

function handleAppData(referenceId: string, params: any) {
  const appData = params?.appData;
  // Handle data sent from the App Studio app
  console.log('App data received:', appData);
}

/v1/onFrameSizeChange — Frame Resize

Fired when embed content resizes. Use to adjust iframe container:

function handleFrameResize(referenceId: string, params: any) {
  const { width, height } = params || {};
  const iframe = document.querySelector(`iframe[name="${referenceId}"]`);
  if (iframe && height) {
    (iframe as HTMLIFrameElement).style.height = `${height}px`;
  }
}

Sending Data to App Studio Apps

Pass arbitrary context to App Studio apps via /v1/appData/apply:

export const sendAppData = (referenceId: string, appData: any) => {
  const port = ports[referenceId];
  if (!port) return;

  port.postMessage({
    id: `appData-${Date.now()}`,
    jsonrpc: '2.0',
    method: '/v1/appData/apply',
    params: { appData }
  });
};

The embedded app receives this via domo.onAppData(). Use for passing user IDs, customer context, or preferences.


PFilter URL Parameters

A no-JS alternative: append filters as URL query params. Trade-off: triggers full iframe refresh and filters are visible/modifiable in the URL.

Structure

?pfilters=[{"column":"ColumnName","operand":"OPERATOR","values":["val1","val2"]}]

Example

https://example.domo.com/embed/pages/private/ABCDE?pfilters=[{"column":"Region","operand":"IN","values":["West","East"]},{"column":"Year","operand":"EQUALS","values":["2025"],"dataType":"NUMBER"}]

Optional Fields

  • dataSourceId — target a specific dataset
  • dataType — STRING, NUMBER, DATE

When to Use PFilters vs JS API

PFiltersJS API
Setup requiredNoneMessagePort initialization
SpeedSlower (iframe refresh)Instant (no reload)
SecurityVisible in URL, user-modifiableVisible in DOM, user-modifiable
Multiple updatesEach change refreshes iframeUpdates in place
Best forSimple one-time filters, link sharingInteractive filtering, dynamic UIs

Security warning: PFilters are visible in the URL and can be modified by end users. They provide no security — use server-side programmatic filters for data isolation.


overrideFilters for App Studio

The overrideFilters=true URL parameter tells App Studio apps to ignore saved defaults and wait for host-provided filters:

https://example.domo.com/embed/pages/private/ABCDE?overrideFilters=true

Behavior:

  • overrideFilters=true — app loads with no filters, waits for host to send them
  • overrideFilters=false or omitted — app loads its saved defaults normally

Important: You must send at least one /v1/filters/apply (even empty []) or the app stays in a waiting state. Listen for /v1/onAppReady first.

function handleAppReady(referenceId: string, params: any) {
  // App is ready — send the initial filter state
  applyFiltersToEmbed(referenceId, userFilters.length > 0 ? userFilters : []);
}

Other useful URL parameters for embedded content:

ParameterPurpose
transparentBackground=trueMakes dashboard background transparent
appData=valuePass initial context to App Studio apps (spaces as +)
viewId=abc123Specify which App Studio page view to display

Known RPC Methods

Host → Domo (Commands)

MethodDescription
/v1/filters/applyApply or clear runtime filters
/v1/appData/applySend context data to App Studio apps

Domo → Host (Events)

MethodDescription
/v1/onDrillUser drilled into a data point
/v1/onFiltersChangeFilters changed inside the embed
/v1/onAppReadyApp Studio app finished loading, ready for input
/v1/onAppDataApp Studio app sent data back to host
/v1/onFrameSizeChangeEmbedded content changed dimensions

Combining with Server-Side Filters

  • Server-side: Security boundary. Enforced by Domo, user cannot bypass. Use for data isolation.
  • Client-side: UX convenience. User can change. Use for interactive filtering.

Client-side filters narrow within server-side permitted data — they cannot override server-side restrictions.


Gotchas and Best Practices

Port Availability Timing

Calling applyFilters before the handshake completes silently fails. Two approaches:

Dashboards/cards: Queue and flush on connect:

const pendingFilters: Record<string, Array<{ column: string; operand: string; values: any[] }>> = {};

// When storing the port after handshake, flush pending filters
ports[referenceId] = port;
if (pendingFilters[referenceId]) {
  applyFiltersToEmbed(referenceId, pendingFilters[referenceId]);
  delete pendingFilters[referenceId];
}

App Studio apps: Listen for /v1/onAppReady first, then send filters. Required with overrideFilters=true.

operand vs operator

Client-side uses operand, server-side uses operator. Mixing them up causes silent failures.

referenceId Mapping

The referenceId is assigned by the Domo iframe — you don't control it. For multiple embeds, correlate handshake timing with render order. For a single embed, use the broadcast applyFilters().

Operator Typo in Domo API

GREATER_THAN_EQUALS_TO sometimes appears as GREAT_THAN_EQUALS_TO (missing ER). Try both spellings if comparison operators fail.

Multiple Iframes

Each iframe gets its own port. applyFilters broadcasts to all; use applyFiltersToEmbed with a specific referenceId for independent filtering.

Public Embeds Don't Support JS API

Only authenticated (token-based) embeds initiate the MessagePort handshake. Public embed URLs don't work.

Supported Operands

IN, NOT_IN, EQUALS, NOT_EQUALS, GREATER_THAN, GREATER_THAN_EQUALS_TO, LESS_THAN, LESS_THAN_EQUALS_TO, BETWEEN (exactly two values), CONTAINS (partial string match).

No SQL Filters Client-Side

/v1/filters/apply only supports standard filter objects. SQL filters are server-side only.

Security

Client-side filters and pfilters are for UX only — visible and modifiable by end users. Use server-side programmatic filters for data isolation.


TypeScript Type Definitions

type ClientFilterOperand =
  | 'IN'
  | 'NOT_IN'
  | 'EQUALS'
  | 'NOT_EQUALS'
  | 'GREATER_THAN'
  | 'GREATER_THAN_EQUALS_TO'
  | 'LESS_THAN'
  | 'LESS_THAN_EQUALS_TO'
  | 'BETWEEN'
  | 'CONTAINS';

interface ClientFilter {
  column: string;
  operand: ClientFilterOperand;
  values: (string | number | boolean)[];
  dataType?: 'STRING' | 'NUMBER' | 'DATE';
  dataSourceId?: string;
}

interface DomoRpcRequest {
  id: string;
  jsonrpc: '2.0';
  method: string;
  params: Record<string, any>;
}

interface DomoRpcEvent {
  method: string;
  params?: Record<string, any>;
  result?: any;
  error?: { code: number; message: string };
}

interface DrillEventParams {
  filters: ClientFilter[];
}

interface FiltersChangeParams {
  filters: ClientFilter[];
}

interface FrameSizeChangeParams {
  width: number;
  height: number;
}

interface AppDataParams {
  appData: any;
}

Quick Reference

Read references/rpc-methods.md for the full RPC method reference and message format details.

Add the canonical catalog link to the repository README so users can inspect current installs and available audits. The publishing guide covers the complete discovery path.

<a href="https://skillzs.dev/skills/stahura/domo-ai-vibe-rules/jsapi-filters">View jsapi-filters on skillZs</a>