skills/domo-everywhere/edit-embed/SKILL.md
Set up Domo's embedded edit experience — external users create, edit, save, and share Domo content via an embedded iframe. Covers Identity Broker, JWT auth, role-based access (Admin/Privileged/Editor/Participant), instance mapping, and edit URL generation. Use for any Domo edit-mode embedding. Not for read-only embeds (use programmatic-filters) or client-side filtering (use jsapi-filters).
npx skillsauth add stahura/domo-ai-vibe-rules edit-embedInstall 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.
Let external users create, edit, save, and share Domo content (dashboards, cards, alerts, reports, data sources) through an embedded iframe. Uses the Domo Identity Broker with JWT auth — fundamentally different from read-only embeds.
For read-only embeds, see programmatic-filters. For client-side filtering, see jsapi-filters.
{IDP_URL}/jwt?token={jwt_token}src (no POST form needed)Key difference: read-only = OAuth → embed token → POST form. Edit = JWT → Identity Broker URL → iframe src.
Work with your Domo CSM to set up the Identity Broker. Provide: your Domo instance URL, auth method (JWT), routing attribute key, and attribute-to-instance mappings.
You receive: Identity Broker URL, JWT signing secret (UUID), and attribute key config.
Required env vars: IDP_URL (Broker URL), JWT_SECRET (signing secret), KEY_ATTRIBUTE (routing attribute name).
Authenticates users (validates JWT signature) and routes them to the correct Domo instance based on the mapping attribute. Supports SAML2, OIDC, JWT, OAuth2 — JWT is most common for embeds.
Each mapping attribute value corresponds to a Domo instance:
| Attribute Value | Domo Instance |
| --------------- | ---------------- |
| acme-corp | acme.domo.com |
| globex | globex.domo.com |
| initech | initech.domo.com |
Configured via your CSM (webform dataset or Excel). Comma-separated values route to multiple instances.
| Field | Type | Description |
| ----------------- | ------------------ | -------------------------------------------------------------------------- |
| sub | string | The user's identifier — typically their email address |
| exp | number | Expiration time (EPOCH timestamp). Keep this short (5 minutes recommended) |
| jti | string | Unique token identifier. Use a UUID v4 to prevent replay attacks |
| {KEY_ATTRIBUTE} | string or string[] | The routing attribute that maps the user to a Domo instance |
| Field | Type | Description |
| ------------- | -------- | ------------------------------------------------------------ |
| name | string | Display name for the user in Domo |
| email | string | User's email address |
| role | string | Domo role: Admin, Privileged, Editor, or Participant |
| employee_id | string | Employee identifier |
| title | string | Job title |
| department | string | Department name |
| location | string | Location |
| phone | string | Phone number |
| locale | string | Locale preference |
| timezone | string | Timezone preference |
| groups | string[] | Group assignments within Domo |
| Role | Capabilities |
| ------------- | ----------------------------------------------------------------------- |
| Admin | Full access — manage users, data, content, and settings |
| Privileged | Create/edit dashboards, cards, dataflows; manage data sources |
| Editor | Create/edit dashboards and cards; limited data source access |
| Participant | View and interact with shared content only (default if omitted) |
Most external users should be Editor or Participant.
Node.js / TypeScript:
import jwt from "jsonwebtoken";
import { v4 as uuidv4 } from "uuid";
function createEditToken(user: {
username: string;
email: string;
domoRole?: string;
mappingValue?: string | string[];
}) {
const jwtBody: Record<string, unknown> = {
sub: user.username,
name: user.username,
email: user.email,
role: user.domoRole || "Participant",
jti: uuidv4(),
};
// Add the routing attribute for instance mapping
const keyAttribute = process.env.KEY_ATTRIBUTE;
if (keyAttribute && user.mappingValue) {
jwtBody[keyAttribute] = user.mappingValue;
}
return jwt.sign(jwtBody, process.env.JWT_SECRET!, {
expiresIn: "5m",
algorithm: "HS256",
});
}
Python:
import jwt
import uuid
import time
import os
def create_edit_token(user):
payload = {
'sub': user['username'],
'name': user['username'],
'email': user['email'],
'role': user.get('domo_role', 'Participant'),
'jti': str(uuid.uuid4()),
'exp': int(time.time()) + 300 # 5 minutes
}
key_attribute = os.environ.get('KEY_ATTRIBUTE')
if key_attribute and user.get('mapping_value'):
payload[key_attribute] = user['mapping_value']
return jwt.encode(payload, os.environ['JWT_SECRET'], algorithm='HS256')
const editUrl = `${process.env.IDP_URL}/jwt?token=${editToken}`;
Optionally deep-link to a specific page:
const editUrl = `${process.env.IDP_URL}/jwt?token=${editToken}&destination=/page/${pageId}`;
Next.js App Router:
// app/api/editembed/route.ts
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import jwt from "jsonwebtoken";
import { v4 as uuidv4 } from "uuid";
export async function POST(req: NextRequest) {
try {
const { embedID } = await req.json();
// Authenticate the user against your own auth system
const token = req.cookies.get("token")?.value;
if (!token) {
return NextResponse.json(
{ message: "Unauthorized: Please log in" },
{ status: 401 },
);
}
const user = await verifyAndGetUser(token);
if (!user) {
return NextResponse.json({ message: "User not found" }, { status: 404 });
}
// Handle comma-separated mapping values
let mappingValue = user.mappingValue;
if (typeof mappingValue === "string" && mappingValue.includes(",")) {
mappingValue = mappingValue.split(",").map((s: string) => s.trim());
}
// Build the JWT payload
const jwtBody: Record<string, unknown> = {
sub: user.username,
name: user.username,
role: user.domoRole || "Participant",
email: user.email,
jti: uuidv4(),
};
const keyAttr = process.env.KEY_ATTRIBUTE;
if (keyAttr && mappingValue) {
jwtBody[keyAttr] = mappingValue;
}
// Sign the token
const editToken = jwt.sign(jwtBody, process.env.JWT_SECRET || "", {
expiresIn: "5m",
});
// Return the Identity Broker URL
const editUrl = `${process.env.IDP_URL}/jwt?token=${editToken}`;
return NextResponse.json(editUrl);
} catch (error) {
console.error("Error in /api/editembed:", error);
return NextResponse.json(
{ message: "Server error occurred" },
{ status: 500 },
);
}
}
Express example:
app.post("/api/editembed", authenticateUser, (req, res) => {
const user = req.user;
let mappingValue = user.mappingValue;
if (typeof mappingValue === "string" && mappingValue.includes(",")) {
mappingValue = mappingValue.split(",").map((s) => s.trim());
}
const jwtBody = {
sub: user.username,
name: user.username,
role: user.domoRole || "Participant",
email: user.email,
jti: uuidv4(),
};
const keyAttr = process.env.KEY_ATTRIBUTE;
if (keyAttr && mappingValue) {
jwtBody[keyAttr] = mappingValue;
}
const editToken = jwt.sign(jwtBody, process.env.JWT_SECRET, {
expiresIn: "5m",
});
const editUrl = `${process.env.IDP_URL}/jwt?token=${editToken}`;
res.json(editUrl);
});
Edit embeds load directly via iframe src (no POST form like read-only embeds):
function EditEmbed({ editUrl }: { editUrl: string }) {
return (
<iframe
src={editUrl}
style={{ width: "100%", height: "100%", border: "none" }}
allow="fullscreen"
/>
);
}
If your app supports both read-only and edit modes, your embed component needs to handle both flows:
function EmbedDashboard({ embedID }: { embedID: string }) {
const [embedURL, setEmbedURL] = useState<string | null>(null);
const [embedToken, setEmbedToken] = useState<string | null>(null);
const isEditMode = embedID === "edit";
useEffect(() => {
const endpoint = isEditMode ? "/api/editembed" : "/api/getembedtoken";
fetch(endpoint, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ embedID }),
credentials: "include",
})
.then((res) => res.json())
.then((data) => {
if (isEditMode) {
setEmbedURL(data); // Edit returns a direct URL string
setEmbedToken(null);
} else {
setEmbedURL(data.embedUrl); // Read-only returns { embedUrl, embedToken }
setEmbedToken(data.embedToken);
}
});
}, [embedID, isEditMode]);
// Edit mode: direct iframe src
if (isEditMode && embedURL) {
return (
<iframe
src={embedURL}
style={{ width: "100%", height: "100%", border: "none" }}
/>
);
}
// Read-only mode: POST form submission to iframe
// (handle embedToken + form submission as in programmatic-filters skill)
}
| Property | Purpose | Example |
| -------------- | ------------------------------------ | ------------------------------------------ |
| domoRole | Controls edit capabilities | 'Editor', 'Participant' |
| mappingValue | Routes user to correct Domo instance | 'acme-corp' or ['acme-corp', 'globex'] |
| email | Required for Domo user identity | '[email protected]' |
If a user needs access to multiple Domo instances, store their mappingValue as a comma-separated string or array:
// Single instance
user.mappingValue = "acme-corp";
// Multiple instances
user.mappingValue = "acme-corp, globex";
// or
user.mappingValue = ["acme-corp", "globex"];
When building the JWT, handle both formats:
let mappingValue = user.mappingValue;
if (typeof mappingValue === "string" && mappingValue.includes(",")) {
mappingValue = mappingValue.split(",").map((s) => s.trim());
}
const editUrl = `${IDP_URL}/jwt?token=${editToken}&destination=/page/${pageId}`;
const editUrl = `${IDP_URL}/jwt?token=${editToken}&destination=/kpicard/${cardId}`;
HS256. Domo expects this for the Identity Broker.KEY_ATTRIBUTE name is whatever you agreed on with your CSM (customer, keyAttribute, tenant). Values must match Domo's instance mapping exactly.JWT_SECRET client-side. Use env vars or a secrets manager.| Variable | Description | Example |
| --------------- | ----------------------------------- | --------------------------------------- |
| IDP_URL | Identity Broker URL | https://yourcompany.identity.domo.com |
| JWT_SECRET | Shared signing secret (UUID format) | aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeee |
| KEY_ATTRIBUTE | Attribute key for instance routing | keyAttribute, customer, tenant |
type DomoRole = "Admin" | "Privileged" | "Editor" | "Participant";
interface EditEmbedUser {
username: string;
email: string;
domoRole?: DomoRole;
mappingValue?: string | string[];
}
interface EditJwtPayload {
sub: string;
name?: string;
email?: string;
role?: DomoRole;
jti: string;
exp?: number;
[keyAttribute: string]: unknown; // dynamic routing attribute
}
Read references/identity-broker.md for additional details on Identity Broker configuration and instance mapping management.
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.