skills/domo-everywhere/programmatic-filters/SKILL.md
Control which data each viewer sees in an embedded Domo dashboard/card via server-side programmatic filters and dataset switching. Covers the OAuth → embed token flow, standard filters, SQL filters (OR/BETWEEN/LIKE), per-dataset targeting, datasetRedirects for multi-tenant architectures, and token size limits. Use for any per-viewer, per-role, or per-tenant data restrictions at embed time. Not for client-side JS API filtering (use jsapi-filters).
npx skillsauth add stahura/domo-ai-vibe-rules programmatic-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.
Control what data each viewer sees in embedded Domo content via server-side filters and dataset switching. Enforced by Domo — end users can't bypass. For client-side filtering, see jsapi-filters.
A proxy user (service account) acts on behalf of all viewers. Your server:
CLIENT_ID and CLIENT_SECRET (developer.domo.com > My Account > New Client)All auth must happen server-side (CORS restrictions). Never expose credentials client-side.
POST https://api.domo.com/oauth/token?grant_type=client_credentials&scope=data%20audit%20user%20dashboard
Authorization: Basic base64(CLIENT_ID:CLIENT_SECRET)
Node.js:
const credentials = Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString('base64')
const response = await fetch(
'https://api.domo.com/oauth/token?grant_type=client_credentials&scope=data%20audit%20user%20dashboard',
{
method: 'GET',
headers: { Authorization: `Basic ${credentials}` }
}
)
const { access_token } = await response.json()
Python:
import requests
from base64 import b64encode
credentials = b64encode(f"{CLIENT_ID}:{CLIENT_SECRET}".encode()).decode()
response = requests.get(
"https://api.domo.com/oauth/token",
params={"grant_type": "client_credentials", "scope": "data audit user dashboard"},
headers={"Authorization": f"Basic {credentials}"}
)
access_token = response.json()["access_token"]
Use the access token to request an embed token, including your filters in the authorizations payload:
For dashboards:
POST https://api.domo.com/v1/stories/embed/auth
Authorization: Bearer {access_token}
Content-Type: application/json
For cards:
POST https://api.domo.com/v1/cards/embed/auth
Authorization: Bearer {access_token}
Content-Type: application/json
Payload structure:
{
"sessionLength": 1440,
"authorizations": [
{
"token": "<embed_id>",
"permissions": ["READ", "FILTER", "EXPORT"],
"filters": [],
"policies": []
}
]
}
sessionLength: Token validity in minutes (1440 = 24 hours)token: The embed ID for the dashboard or cardpermissions: Array of granted permissions — READ, FILTER, EXPORTfilters: Array of standard filter objects (see below)policies: Array of PDP policy IDs to apply (optional)The response includes an authentication property containing the embed token.
Submit via hidden POST form targeting an iframe. Dashboard: https://public.domo.com/embed/pages/{embed_id}, Card: https://public.domo.com/cards/{embed_id}
<iframe id="domo-embed" name="domo-embed" width="100%" height="600"></iframe>
<form id="embed-form" action="https://public.domo.com/embed/pages/{embed_id}" method="POST" target="domo-embed">
<input type="hidden" name="embedToken" value="{embed_token}" />
</form>
<script>document.getElementById('embed-form').submit();</script>
POST submission prevents the token from appearing in URLs or browser history.
Two types: standard filters and SQL filters, both in the embed token request.
{
"column": "Region",
"operator": "IN",
"values": ["West", "East"]
}
Required properties:
| Property | Type | Description |
|----------|------|-------------|
| column | string | The exact column name in the dataset |
| operator | string | The comparison operator (see table below) |
| values | array | Values to filter against |
Optional properties:
| Property | Type | Description |
|----------|------|-------------|
| datasourceId | string (UUID) | Restrict filter to a specific dataset. Without this, the filter applies to all datasets in the embed that have a matching column name. |
| Operator | Description | Typical Use |
|----------|-------------|-------------|
| IN | Column value is in the list | Multi-value match |
| NOT_IN | Column value is not in the list | Exclusion |
| EQUALS | Column value equals | Single-value exact match |
| NOT_EQUALS | Column value does not equal | Single-value exclusion |
| GREATER_THAN | Column value is greater than | Numeric/date range |
| GREATER_THAN_EQUALS_TO | Column value is greater than or equal | Numeric/date range |
| LESS_THAN | Column value is less than | Numeric/date range |
| LESS_THAN_EQUALS_TO | Column value is less than or equal | Numeric/date range |
Single filter — show only West region:
{
"filters": [
{
"column": "Region",
"operator": "IN",
"values": ["West"]
}
]
}
Multiple filters — West region, revenue over 10000:
{
"filters": [
{
"column": "Region",
"operator": "IN",
"values": ["West"]
},
{
"column": "Revenue",
"operator": "GREATER_THAN",
"values": [10000]
}
]
}
Dataset-specific filter — only apply to one dataset in a multi-dataset dashboard:
{
"filters": [
{
"column": "Region",
"operator": "IN",
"values": ["West"],
"datasourceId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
]
}
For complex logic (OR, BETWEEN, LIKE, nested expressions), use SQL WHERE syntax via sqlFilters (sibling to filters, not nested).
{
"authorizations": [
{
"token": "<embed_id>",
"permissions": ["READ", "FILTER", "EXPORT"],
"filters": [],
"sqlFilters": [
{
"sqlFilter": "`Region` IN ('West', 'East') AND `Revenue` > 10000",
"datasourceIds": ["xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"]
}
]
}
]
}
Required properties:
| Property | Type | Description |
|----------|------|-------------|
| sqlFilter | string | SQL WHERE clause syntax |
Optional properties:
| Property | Type | Description |
|----------|------|-------------|
| datasourceIds | string[] | Array of dataset UUIDs to apply the filter to |
Column names in backticks, strings in single quotes, numerics unquoted. Supports: IN, NOT IN, BETWEEN, LIKE, AND, OR, IS NULL, IS NOT NULL. Use parentheses for grouping.
OR condition (not possible with standard filters alone):
{
"sqlFilter": "`Region` = 'West' OR `Department` = 'Sales'"
}
BETWEEN for date ranges:
{
"sqlFilter": "`Order Date` BETWEEN '2024-01-01' AND '2024-12-31'"
}
LIKE for partial matching:
{
"sqlFilter": "`Product Name` LIKE '%Pro%'"
}
Complex nested logic:
{
"sqlFilter": "(`Region` IN ('West', 'East') AND `Revenue` > 10000) OR `Priority` = 'Critical'"
}
Targeting specific datasets:
{
"sqlFilter": "`Status` = 'Active'",
"datasourceIds": [
"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy"
]
}
Both can coexist in one authorization. Standard filters apply first, then SQL filters narrow further:
{
"token": "<embed_id>",
"permissions": ["READ", "FILTER", "EXPORT"],
"filters": [
{
"column": "Region",
"operator": "IN",
"values": ["West"]
}
],
"sqlFilters": [
{
"sqlFilter": "`Revenue` > 10000 OR `Priority` = 'Critical'"
}
]
}
Store user-to-filter mappings in your database, look up at embed time:
// Pseudocode — adapt to your framework and data layer
async function getEmbedTokenForUser(userId: string, embedId: string) {
const user = await getUser(userId)
const dashboard = user.dashboards.find(d => d.embedId === embedId)
const filters = dashboard.filters.map(f => ({
column: f.column,
operator: f.operator,
values: f.values
}))
const accessToken = await getDomoAccessToken()
const embedToken = await getDomoEmbedToken(accessToken, embedId, filters)
return embedToken
}
Apply different filters based on user role rather than individual assignments:
function getFiltersForRole(role: string): Filter[] {
switch (role) {
case 'regional-manager':
return [{ column: 'Region', operator: 'IN', values: [user.region] }]
case 'executive':
return [] // No filters — sees everything
case 'analyst':
return [{ column: 'Department', operator: 'EQUALS', values: [user.department] }]
default:
return [{ column: 'Public', operator: 'EQUALS', values: ['true'] }]
}
}
Redirect the underlying dataset of an embedded dashboard at embed time — useful for multi-tenant architectures where each customer has their own dataset with the same schema.
Pass datasetRedirects in the authorization object (original dataset ID → target dataset ID):
{
"sessionLength": 1440,
"authorizations": [
{
"token": "<embed_id>",
"permissions": ["READ", "FILTER", "EXPORT"],
"filters": [],
"datasetRedirects": {
"original-dataset-uuid-1": "target-dataset-uuid-1",
"original-dataset-uuid-2": "target-dataset-uuid-2"
}
}
]
}
Target datasets must match the original schema (column names and types).
Redirects apply first, then filters run against the swapped dataset:
{
"sessionLength": 1440,
"authorizations": [
{
"token": "<embed_id>",
"permissions": ["READ", "FILTER", "EXPORT"],
"datasetRedirects": {
"a19d0ef1-ca31-4bfd-b168-018b93109671": "a19d0ef1-ca31-4bfd-b168-018b93109672"
},
"filters": [
{ "column": "Color", "operator": "IN", "values": ["Red"] },
{ "column": "Model", "operator": "IN", "values": ["Mountain", "Road", "Commuter"] }
]
}
]
}
Map each tenant to their dataset IDs:
function getDatasetRedirects(tenant: Tenant): Record<string, string> {
// The template dashboard was built on these "original" datasets
const templateDatasets = {
sales: 'aaaa-bbbb-cccc-dddd',
inventory: 'eeee-ffff-gggg-hhhh'
}
// Each tenant has their own copies
return {
[templateDatasets.sales]: tenant.salesDatasetId,
[templateDatasets.inventory]: tenant.inventoryDatasetId
}
}
datasourceId targeting.datasourceId, filters apply to all datasets with a matching column.[] means no filtering (user sees everything). Always include the filters key — don't omit it.values must be an array (even for EQUALS). Numerics must be numbers, not strings. Operator names are exact (GREATER_THAN_EQUALS_TO, not GREATER_THAN_OR_EQUAL).permissions (omit EXPORT if not needed).type FilterOperator =
| 'IN'
| 'NOT_IN'
| 'EQUALS'
| 'NOT_EQUALS'
| 'GREATER_THAN'
| 'GREATER_THAN_EQUALS_TO'
| 'LESS_THAN'
| 'LESS_THAN_EQUALS_TO'
interface StandardFilter {
column: string
operator: FilterOperator
values: (string | number)[]
datasourceId?: string
}
interface SqlFilter {
sqlFilter: string
datasourceIds?: string[]
}
type EmbedPermission = 'READ' | 'FILTER' | 'EXPORT'
interface EmbedAuthorization {
token: string
permissions: EmbedPermission[]
filters: StandardFilter[]
sqlFilters?: SqlFilter[]
policies?: string[]
datasetRedirects?: Record<string, string> // originalDatasetId → targetDatasetId
}
interface EmbedTokenRequest {
sessionLength: number
authorizations: EmbedAuthorization[]
}
Read references/api-endpoints.md for the complete list of Domo API endpoints used in programmatic filtering.
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.