AI agents automate everyday work like reporting, alerts, and approvals. You can build agents that respond to data changes, adapt to your workflows, and help teams take action without manual effort.
Explore and share data using AI
Analyze data, uncover trends, and answer questions using secure data and conversational AI. Embed AI Chat into dashboards and content so users can ask questions and get clear answers where they already work.
Use AI-assisted workflows to automate decisions and actions
Use AI-assisted workflows to build processes, analyze data, and forecast outcomes without added complexity. These tools reduce manual work so teams can focus on acting on results instead of managing workflows.
Prepare and organize data so AI works reliably
Clean, organize, and structure your data so AI delivers accurate results. Validate key details and create a reliable data foundation that teams can trust when using AI.
Custom and pre-built agents for real business tasks
“One of the main reasons we use Domo as our data platform is the ease of demonstrating value at all levels across the global organization, from frontline workers to senior executives.”
Get your data ready for analysis by centralizing access across your systems. Connect, prepare, and organize data using built-in tools, while keeping data in your existing cloud environment.
Extend insights beyond dashboards
Share data with more people through visualizations, apps, and AI-powered data products. Make insights ready to teams, users, and customers, even outside of Domo.
Get more done with automation
Use built-in automation to stay informed, streamline operations, and reduce errors. Trigger alerts, workflows, and actions so teams can focus on high-impact work.
Secure and govern your data
Keep your data secure, compliant, and easy to manage. Use built-in governance, access controls, and alerts to protect sensitive data, maintain quality, and reduce risk as more teams use Domo.
Build custom AI agents and automations using your data
Build Your First Competitive Research Agent in Domo
Domo works with leading cloud, data, and application platforms
Use Domo with your existing data warehouse to analyze data and make decisions faster. Connect to leading cloud platforms and work with your data where it already lives.
), loads on any page.
* Purpose: mirror the values DomoForms currently sends to Eloqua and push
* them into the Qualified chat via qualified("identify", {...}).
* Qualified then syncs them to Eloqua.
* Depends on: nothing. Fully self-contained. Does NOT require DomoForms.
* Runs only when window.qualified exists (chat present on the page).
*
* v1.1.0: added CONFIG.gate - test-phase gate.
* v1.2.0: log everything pushed to qualified (console.table + raw JSON).
* v1.3.0: gate is now OR-based: identify runs if the page path is on the
* allowlist (regardless of query) OR the override query param
* (?test=piperx) is present on ANY page.
* v1.4.0: all console output is gated behind ?consolelog=true. Silent by
* default. Set gate.enabled=false to ship fully site-wide.
* ========================================================= */
(function () {
'use strict';
if (window.__domoQualifiedLoaded) return;
window.__domoQualifiedLoaded = true;
var LOG = '[DomoQualified]';
// Console output only when ?consolelog=true is present in the URL.
var DEBUG = new URLSearchParams(window.location.search).get('consolelog') === 'true';
var log = {
log: function () { if (DEBUG) console.log.apply(console, arguments); },
group: function () { if (DEBUG && console.group) console.group.apply(console, arguments); },
groupEnd: function () { if (DEBUG && console.groupEnd) console.groupEnd(); },
table: function () { if (DEBUG && console.table) console.table.apply(console, arguments); }
};
var CONFIG = {
// ---- TEST-PHASE GATE -----------------------------------------------
// identify() runs when EITHER of these is true:
// 1) the current path matches one of `paths` (query ignored), OR
// 2) the override query param is present (?test=piperx) on ANY page.
// Paths are matched as case-insensitive substrings, so '/pricing' also
// covers localized variants like '/de/pricing'.
// To ship fully site-wide later: set enabled:false (one line).
gate: {
enabled: true,
paths: ['/pricing'], // always-on pages. [] = no path is auto-allowed
param: 'test', // override query param name. '' = disable override
paramValue: 'piperx' // required value. '' = param just has to be present
},
// --------------------------------------------------------------------
// Qualified's snippet may load async / from
, so we poll for it.
readyIntervalMs: 250,
readyTimeoutMs: 15000,
// Don't push blank values (avoids overwriting good data with "").
omitEmpty: true,
// Keep one uniqueFFID per browser session so the chat reuses the same ID
// across page navigations. A URL param (?unique_ffid=) always wins.
persistFFID: true,
ffidSessionKey: 'domo_qualified_ffid',
// What to send as `fullurlpath`. See open question in the handoff notes.
// 'utm_query' -> window.location.search (== DomoForms utmquerystring1) [default]
// 'href' -> window.location.href (== DomoForms pathName1)
// 'path_query' -> location.pathname + search
fullUrlSource: 'utm_query',
cookies: {
domoId: 'did', // Domo ID cookie (same as DomoForms)
utm: '_pubweb_utm', // fallback UTM query string cookie (same as DomoForms)
ga: '_ga' // GA client-id cookie
},
// Qualified field name -> internal resolver key
fieldMap: {
domo_id: 'domoId',
unique_form_fill_id: 'uniqueFFID',
fullurlpath: 'fullUrl',
marketing_cloud_id: 'marketingCloudId',
google_analytics_id: 'gaClientId',
language: 'language'
}
};
/* ---------- test-phase gate (OR logic) ---------- */
function paramOverridePresent() {
var g = CONFIG.gate;
if (!g.param) return false;
var v = new URLSearchParams(window.location.search).get(g.param);
if (v === null) return false; // param absent
return g.paramValue ? v === g.paramValue // must equal expected value
: true; // any value is fine
}
function pathAllowed() {
var g = CONFIG.gate;
if (!g.paths || !g.paths.length) return false;
var path = window.location.pathname.toLowerCase();
for (var i = 0; i < g.paths.length; i++) {
var p = g.paths[i];
if (p && path.indexOf(p.toLowerCase()) !== -1) return true;
}
return false;
}
function passesGate() {
if (!CONFIG.gate || !CONFIG.gate.enabled) return true; // site-wide mode
return pathAllowed() || paramOverridePresent(); // OR
}
/* ---------- cookie / url utils (identical behavior to DomoForms) ---------- */
function getCookie(name) {
var parts = ('; ' + document.cookie).split('; ' + name + '=');
return parts.length === 2 ? parts.pop().split(';').shift() : '';
}
function setCookie(name, value, days) {
var expires = '';
if (days) {
var d = new Date();
d.setTime(d.getTime() + days * 86400000);
expires = '; expires=' + d.toUTCString();
}
document.cookie = name + '=' + (value || '') + expires + '; path=/';
}
function getUrlParam(name) {
return new URLSearchParams(window.location.search).get(name) || '';
}
/* ---------- domo_id (getOrCreateDomoID, same as DomoForms) ---------- */
function generateDomoID() {
var t = Math.floor(Date.now() / 1000);
var r = Math.floor(Math.random() * 1e8);
return (t * r).toString().slice(0, 10);
}
function getOrCreateDomoID() {
var d = getCookie(CONFIG.cookies.domoId);
if (!d || d === 'undefined' || d.length < 10 || +d === 0) {
d = generateDomoID();
setCookie(CONFIG.cookies.domoId, d, 3650);
}
return d;
}
/* ---------- google_analytics_id (raw _ga cookie value, same as DomoForms g_id) ----------
* DomoForms sends the RAW _ga value, e.g. "GA1.2.1234567890.0987654321".
* If Eloqua actually wants only the client-id ("1234567890.0987654321"),
* strip the "GA1.2." prefix here. Left raw for 1:1 parity with the form. */
function getGaClientId() {
var rows = document.cookie.split('; ');
for (var i = 0; i < rows.length; i++) {
if (rows[i].indexOf(CONFIG.cookies.ga + '=') === 0) {
return rows[i].split('=')[1] || '';
}
}
return '';
}
/* ---------- fullurlpath ---------- */
function getFullUrl() {
if (CONFIG.fullUrlSource === 'href') return window.location.href;
if (CONFIG.fullUrlSource === 'path_query') return window.location.pathname + window.location.search;
// default 'utm_query' == DomoForms "Q": location.search, else _pubweb_utm cookie
return window.location.search || getCookie(CONFIG.cookies.utm) || '';
}
/* ---------- language (navigator.language -> en-US / de-DE / fr-FR) ----------
* DomoForms sends navigator.language (BROWSER language, not the page locale).
* If Qualified/Eloqua should get the PAGE language on localized pages, use:
* return document.documentElement.lang || navigator.language || ''; */
function getLanguage() {
return navigator.language || '';
}
/* ---------- marketing_cloud_id ----------
* NOTE: DomoForms does NOT populate this today (no such field in the framework).
* This is a best-effort read of the Adobe Experience Cloud ID (a.k.a.
* "Marketing Cloud ID" / MID / ECID). Returns '' until the real source is
* confirmed. If it comes from somewhere else (SFMC subscriber key, a hidden
* Eloqua field, etc.), swap this out. */
function getMarketingCloudId() {
try {
if (window.visitor && typeof window.visitor.getMarketingCloudVisitorID === 'function') {
return window.visitor.getMarketingCloudVisitorID() || '';
}
} catch (e) {}
var ecid = getCookie('s_ecid'); // format: "MCMID|"
if (ecid && ecid.indexOf('MCMID|') === 0) return ecid.split('|')[1] || '';
var amcv = document.cookie.match(/AMCV_[^=]+=([^;]+)/); // "...|MCMID||..."
if (amcv && amcv[1]) {
var m = decodeURIComponent(amcv[1]).match(/MCMID\|(\d+)/);
if (m) return m[1];
}
return '';
}
/* ---------- unique_form_fill_id (same pattern as DomoForms _getUniqueFFID) ---------- */
function hashSHA1(input) {
if (!window.crypto || !crypto.subtle) {
return Promise.resolve(fallbackId());
}
var bytes = new TextEncoder().encode(input);
return crypto.subtle.digest('SHA-1', bytes).then(function (buf) {
var arr = Array.prototype.slice.call(new Uint8Array(buf));
// zero-padded 40-char hex (DomoForms omits padding; value is independent
// per fill so it never needs to match the form's value byte-for-byte)
return arr.map(function (b) { return ('0' + b.toString(16)).slice(-2); }).join('');
});
}
function fallbackId() {
return (window.crypto && crypto.randomUUID) ? crypto.randomUUID() : String(Date.now()) + String(Math.random()).slice(2);
}
function getUniqueFFID() {
// 1) explicit URL param wins (matches DomoForms)
var p = getUrlParam('unique_ffid') || getUrlParam('uniqueFFID');
if (p) return Promise.resolve(p);
// 2) reuse session value if present
if (CONFIG.persistFFID) {
try {
var stored = sessionStorage.getItem(CONFIG.ffidSessionKey);
if (stored) return Promise.resolve(stored);
} catch (e) {}
}
// 3) generate SHA1(randomUUID | domo_id)
var domo = getUrlParam('domo_id') || getCookie(CONFIG.cookies.domoId) || '';
return hashSHA1(fallbackId() + '|' + domo).then(function (ffid) {
if (CONFIG.persistFFID) {
try { sessionStorage.setItem(CONFIG.ffidSessionKey, ffid); } catch (e) {}
}
return ffid;
});
}
/* ---------- resolvers ---------- */
var resolvers = {
domoId: function () { return Promise.resolve(getOrCreateDomoID()); },
uniqueFFID: function () { return getUniqueFFID(); },
fullUrl: function () { return Promise.resolve(getFullUrl()); },
marketingCloudId: function () { return Promise.resolve(getMarketingCloudId()); },
gaClientId: function () { return Promise.resolve(getGaClientId()); },
language: function () { return Promise.resolve(getLanguage()); }
};
function buildFieldValues() {
var qfields = Object.keys(CONFIG.fieldMap);
return Promise.all(qfields.map(function (qfield) {
var fn = resolvers[CONFIG.fieldMap[qfield]];
return (fn ? fn() : Promise.resolve('')).then(function (val) {
return { field: qfield, value: (val == null ? '' : String(val)) };
});
})).then(function (pairs) {
var out = {};
for (var i = 0; i < pairs.length; i++) {
if (CONFIG.omitEmpty && pairs[i].value === '') continue;
out[pairs[i].field] = pairs[i].value;
}
return out;
});
}
/* ---------- console logging of what we push (only when DEBUG) ---------- */
function logFieldValues(fieldValues) {
if (!DEBUG) return;
try {
log.group(LOG + ' pushing to qualified("identify")');
var rows = Object.keys(fieldValues).map(function (k) {
return { field: k, value: fieldValues[k] };
});
if (rows.length) { log.table(rows); }
else { log.log(LOG, fieldValues); }
log.log(LOG, 'raw object:', JSON.stringify(fieldValues, null, 2));
log.groupEnd();
} catch (e) {
log.log(LOG, 'field values:', fieldValues);
}
}
/* ---------- wait for Qualified ---------- */
function whenQualifiedReady(cb) {
if (typeof window.qualified === 'function') { cb(); return; }
var waited = 0;
var timer = setInterval(function () {
if (typeof window.qualified === 'function') {
clearInterval(timer);
cb();
} else {
waited += CONFIG.readyIntervalMs;
if (waited >= CONFIG.readyTimeoutMs) {
clearInterval(timer);
log.log(LOG, 'window.qualified not found within ' + CONFIG.readyTimeoutMs + 'ms - skipping identify.');
}
}
}, CONFIG.readyIntervalMs);
}
/* ---------- run ----------
* identify(opts): opts.force === true bypasses the test gate (for QA). */
function identify(opts) {
var force = !!(opts && opts.force);
if (!force && !passesGate()) {
log.log(LOG, 'gate not met (not an allowlisted path and no ?' + CONFIG.gate.param + '=' + CONFIG.gate.paramValue + ') - skipping identify. Call DomoQualified.identify({force:true}) to override.');
return;
}
whenQualifiedReady(function () {
buildFieldValues().then(function (fieldValues) {
logFieldValues(fieldValues);
try {
window.qualified('identify', fieldValues);
log.log(LOG, 'identify sent OK');
} catch (e) {
log.log(LOG, 'identify failed:', e);
}
});
});
}
// small debug/QA hook (safe to leave in)
window.DomoQualified = {
version: '1.4.0',
identify: identify, // DomoQualified.identify({force:true}) to bypass gate
getValues: buildFieldValues, // DomoQualified.getValues().then(console.log)
passesGate: passesGate, // DomoQualified.passesGate() -> true/false
config: CONFIG
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function () { identify(); }, { once: true });
} else {
identify();
}
})();