skills/domo-everywhere/jsapi-filters/SKILL.md
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).
npx skillsauth add stahura/domo-ai-vibe-rules jsapi-filtersInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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.
Domo iframes communicate with the host page via MessagePort (JSON-RPC 2.0):
message event with a MessagePort and referenceIdSet 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);
};
};
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>
)
}
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.
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.
Send /v1/filters/apply through the stored MessagePort.
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
}
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 }
});
};
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([]);
'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>
)
}
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 EventsFired 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 EventsFired 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 ReadyFired 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 DomoFired 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 ResizeFired 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`;
}
}
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.
A no-JS alternative: append filters as URL query params. Trade-off: triggers full iframe refresh and filters are visible/modifiable in the URL.
?pfilters=[{"column":"ColumnName","operand":"OPERATOR","values":["val1","val2"]}]
https://example.domo.com/embed/pages/private/ABCDE?pfilters=[{"column":"Region","operand":"IN","values":["West","East"]},{"column":"Year","operand":"EQUALS","values":["2025"],"dataType":"NUMBER"}]
dataSourceId — target a specific datasetdataType — STRING, NUMBER, DATE| | PFilters | JS API | |---|---------|--------| | Setup required | None | MessagePort initialization | | Speed | Slower (iframe refresh) | Instant (no reload) | | Security | Visible in URL, user-modifiable | Visible in DOM, user-modifiable | | Multiple updates | Each change refreshes iframe | Updates in place | | Best for | Simple one-time filters, link sharing | Interactive 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 StudioThe 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 themoverrideFilters=false or omitted — app loads its saved defaults normallyImportant: 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:
| Parameter | Purpose |
|-----------|---------|
| transparentBackground=true | Makes dashboard background transparent |
| appData=value | Pass initial context to App Studio apps (spaces as +) |
| viewId=abc123 | Specify which App Studio page view to display |
| Method | Description |
|--------|-------------|
| /v1/filters/apply | Apply or clear runtime filters |
| /v1/appData/apply | Send context data to App Studio apps |
| Method | Description |
|--------|-------------|
| /v1/onDrill | User drilled into a data point |
| /v1/onFiltersChange | Filters changed inside the embed |
| /v1/onAppReady | App Studio app finished loading, ready for input |
| /v1/onAppData | App Studio app sent data back to host |
| /v1/onFrameSizeChange | Embedded content changed dimensions |
Client-side filters narrow within server-side permitted data — they cannot override server-side restrictions.
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.
Client-side uses operand, server-side uses operator. Mixing them up causes silent failures.
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().
GREATER_THAN_EQUALS_TO sometimes appears as GREAT_THAN_EQUALS_TO (missing ER). Try both spellings if comparison operators fail.
Each iframe gets its own port. applyFilters broadcasts to all; use applyFiltersToEmbed with a specific referenceId for independent filtering.
Only authenticated (token-based) embeds initiate the MessagePort handshake. Public embed URLs don't work.
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).
/v1/filters/apply only supports standard filter objects. SQL filters are server-side only.
Client-side filters and pfilters are for UX only — visible and modifiable by end users. Use server-side programmatic filters for data isolation.
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;
}
Read references/rpc-methods.md for the full RPC method reference and message format details.
tools
Step-by-step orchestrator for building Domo App Studio apps with native KPI cards via community-domo-cli. Sequences app creation, pages, theme, hero metrics, native charts, filter cards, layout assembly, and navigation. CLI-first — no raw API calls.
tools
Create, update, and execute Magic ETL dataflows programmatically via API and CLI. Covers DAG-based JSON dataflow definitions, input/transform/output node wiring, join operations, and execution lifecycle.
tools
Magic ETL dataflows via community-domo-cli — list, get-definition, create, update, run, execution status; JSON DAG actions, transforms, joins. Use when automating dataflows with the community Domo CLI end-to-end. For REST/Java-CLI–first flows or mixed API patterns, use magic-etl instead.
development
Clean, professional dashboard theme for Domo custom apps. CSS custom properties, layout patterns, typography, and design polish that feel native to the Domo platform. Includes OKLCH color palette, layered shadows, concentric border radius, tabular numbers, and micro-interaction patterns.