npx skills add ...
npx skills add exploreomni/omni-cursor-plugin --skill omni-content-builder
npx skills add exploreomni/omni-cursor-plugin --skill omni-content-builder
Create, update, and manage Omni Analytics documents and dashboards programmatically — document lifecycle, tiles, visualizations, filters, and layouts — using the REST API. Use this skill whenever someone wants to build a dashboard, create a workbook, add tiles or charts, configure dashboard filters, update an existing dashboard's model, set up a KPI view, create visualizations, lay out a dashboard, create a document, rename a workbook, delete a dashboard, move a document to a folder, duplicate a dashboard, or any variant of "build a dashboard for", "create a report showing", "add a chart to", "make a dashboard", "update the dashboard layout", "rename this document", "move to folder", or "delete this dashboard". Also use when modifying dashboard-level model customizations like workbook-specific joins or fields.
Create, update, and manage Omni documents and dashboards programmatically via the REST API — document lifecycle, workbook models, filters, and dashboard content.
Tip: Use
omni-model-explorerto understand available fields andomni-content-explorerto find existing dashboards to modify or learn from.
config, visType, or prefersChart are misconfigured. Default to chartType: "table" for reliable rendering, and configure chart visualizations in the Omni UI.identifier not id for all document API calls — .id is null for workbook-type documents and will silently fail.pivots array is present (reported Omni bug). If boolean filters aren't applying, remove the pivot and test again.When unsure whether an endpoint or parameter exists, fetch the OpenAPI spec:
Use this to verify endpoints, available parameters, and request/response schemas before making calls.
Omni dashboards are built from documents (workbooks). Each has:
Documents can be created with full query and visualization configurations via queryPresentations. Fine-tuning tile layout is best done in the Omni UI.
Returns the new document's identifier, workbookId, and dashboardId.
Use queryPresentations to create a document pre-populated with query tabs and visualization configurations.
Doc gap: The create-document API docs mention queryPresentations but don't show the complete structure. This section documents the full format.
| Parameter | Notes |
|---|---|
modelId | Use the base shared model UUID, not a branchId. Get this from the List Models API. |
| Field format | table.field_name or table.field_name[week|month|day|quarter|year] for time granularity |
sorts | column_name must match the exact field string (e.g., "order_items.created_at[month]"), with sort_descending boolean |
| Parameter | Required | Description |
|---|---|---|
name | Yes | Tile/tab title |
topicName | Recommended | Topic name for the query — set this whenever querying from a topic. Ensures correct join context in the dashboard. |
prefersChart | Yes | Must be true to render a chart. Without this, Omni always shows the results table regardless of any other vis settings. |
visType | Yes | Visualization renderer: "omni-kpi" for KPI tiles, "basic" for all standard charts (line, bar, area, scatter, pie, etc.). |
fields | Yes | Duplicate of query.fields — must be present at this level too. |
config | Yes | Chart-specific configuration object. Shape varies by chart type — read a reference dashboard to get the exact structure. |
chartType | No | Optional chart subtype at the presentation level (e.g. "barGrouped"). |
description | No | Tile description. |
query | Yes | Query definition (see below). |
The query object within each query presentation uses the same structure as the Query API:
| Parameter | Required | Description |
|---|---|---|
table | Yes | Base view name |
fields | Yes | Array of view.field_name references (supports timeframe brackets like [month]) |
sorts | No | Array of { "column_name": "...", "sort_descending": bool } |
filters | No | Object of { "field_name": "expression" } — supports "last 90 days", "this quarter", ">100", etc. |
limit | No | Row limit (default 1000, max 50000) |
join_paths_from_topic_name | Recommended | Topic name for join resolution — set this alongside topicName on the parent queryPresentation. |
pivots | No | Array of field names to pivot on |
Note:
modelIdis not needed inside the query object — it's inherited from the document's top-levelmodelId.
visConfig belongs inside the query object — not at the queryPresentation level. When passed as a sibling of query, it is silently dropped by the API.
visConfig alone does not control chart rendering. It stores the chart type hint on the query, but the actual rendering is driven by prefersChart, visType, and config at the queryPresentation level.
chartType values:
| chartType | Visualization |
|---|---|
kpi | KPI / single value |
lineColor | Line chart |
barColor | Bar chart |
areaColor | Area chart |
stackedBarColor | Stacked bar chart |
pie | Pie / donut chart |
scatter | Scatter plot |
heatmap | Heatmap |
map | Map visualization |
table | Data table |
The config object at the queryPresentation level defines the actual chart rendering. Its structure varies by chart type — see references/queryPresentations.md for complete config examples by chart type.
The most reliable way to get the correct config for a given chart type is to build the chart in the Omni UI and read it back via GET /api/v1/documents/{documentId}.
The most reliable way to learn config, visType, and field names is to read an existing dashboard document:
Step 1: Find a reference dashboard
Step 2: Get its full document
Returns the complete queryPresentations array including topicName, visConfig, config, and the full query object for each tile — use this as the source of truth when recreating or templating dashboards.
Tip: Build a reference dashboard in the Omni UI with the chart types and styling you want, then read it via
GET /api/v1/documents/{documentId}to capture the exactqueryPresentationsstructure to use as a template.
model_extension_id from each query object — these reference model extensions scoped to the source document and will cause "Chart unavailable" errors in new documents.GET /api/v1/documents/{id} returns all queries including workbook-only tabs not shown on the dashboard. Only pass the queryPresentations you want as visible tiles.topicName are valid — SQL-mode and tab-selector queries won't have a topicName. Do not add one.get-dashboard-document returns all queries including workbook-only tabs not shown on the dashboard. Pass only the queryPresentations you want as visible tiles — every entry you include will become a visible tile in the new document.model_extension_id: Some queries contain a model_extension_id that references a model extension scoped to the source document. These IDs are not valid in a new document and will cause "Chart unavailable" errors. Remove model_extension_id from each query object before posting.visType: "spreadsheet-tab") will not have a topicName — this is correct, do not add one.Set clearExistingDraft: true if the document has an existing draft, otherwise the API returns 409 Conflict.
Soft-deletes the document (moves to Trash).
Use "folderPath": null to move to root. scope is optional — auto-computed from the destination folder.
Only published documents can be duplicated. Draft documents return 404.
Push custom dimensions and measures to a specific dashboard by writing to its workbook model. This is a two-step flow:
Step 1 — get the document to find its workbook_id:
Step 2 — POST YAML to the workbook model:
fileName must be "model", "relationships", or end with .view or .topic. The yaml value is a YAML string (not a JSON object). Writing to a workbook model skips git sync entirely — authorization is still checked against the underlying shared model's permissions.
Warning:
PUTandPATCHon/dashboards/{id}/filtershave been reported to return 405 or 500 in some configurations. If filter updates fail, include filters during document creation instead (see below).
The most reliable way to create dashboard filters is to include filterConfig and filterOrder in the initial POST /api/v1/documents call. See references/filterConfig.md for complete examples of each filter type.
The keys in filterConfig (e.g., "date_filter") are arbitrary IDs — they must match the entries in filterOrder. To learn the exact filter structure, read filters from an existing dashboard with GET /api/v1/dashboards/{dashboardId}/filters.
Date Range — type: "date", kind: "TIME_FOR_INTERVAL_DURATION"
String Dropdown — type: "string", kind: "EQUALS", values: []
Boolean Toggle — type: "boolean", is_negative: false
Hidden Filter — any filter with "hidden": true (applied but not visible)
Date Granularity Picker — type: "FIELD_SELECTION", kind: "TIMEFRAME" with options array
After creating or finding content, always provide the user a direct link:
The identifier comes from the document's identifier field in API responses (not id, which is null for workbooks).
Aim for minimal API calls. Batch everything into the document creation POST.
omni-model-explorer to find topic + fields (1-2 calls)GET /api/v1/documents/{id} to capture queryPresentations patterns (1 call)POST /api/v1/documents with queryPresentations + filterConfig + filterOrder all in one call{OMNI_BASE_URL}/dashboards/{identifier} to the useromni-model-builder for shared fields, or update-model for dashboard-specific fieldscurl -L -X POST "$OMNI_BASE_URL/api/v1/documents" \
-H "Authorization: Bearer $OMNI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"modelId": "your-model-id",
"name": "Q1 Revenue Report",
"queryPresentations": [
{
"name": "Total Revenue",
"topicName": "order_items",
"prefersChart": true,
"visType": "omni-kpi",
"fields": ["order_items.total_revenue"],
"query": {
"table": "order_items",
"fields": ["order_items.total_revenue"],
"join_paths_from_topic_name": "order_items",
"visConfig": { "chartType": "kpi" }
},
"config": {
"alignment": "left",
"verticalAlignment": "top",
"markdownConfig": [
{
"id": "kpi-1",
"type": "number",
"config": {
"field": {
"row": "_first",
"field": { "name": "order_items.total_revenue", "pivotMap": {} },
"label": { "value": "Total Revenue" }
},
"descriptionBefore": ""
}
}
]
}
},
{
"name": "Monthly Revenue Trend",
"description": "Revenue by month for the current quarter",
"topicName": "order_items",
"prefersChart": true,
"visType": "basic",
"fields": ["order_items.created_at[month]", "order_items.total_revenue"],
"query": {
"table": "order_items",
"fields": [
"order_items.created_at[month]",
"order_items.total_revenue"
],
"sorts": [
{ "column_name": "order_items.created_at[month]", "sort_descending": false }
],
"filters": { "order_items.created_at": "this quarter" },
"limit": 100,
"join_paths_from_topic_name": "order_items",
"visConfig": { "chartType": "lineColor" }
},
"config": {
"x": { "field": { "name": "order_items.created_at[month]" } },
"mark": { "type": "line" },
"color": {},
"series": [{ "field": { "name": "order_items.total_revenue" }, "yAxis": "y" }],
"tooltip": [
{ "field": { "name": "order_items.created_at[month]" } },
{ "field": { "name": "order_items.total_revenue" } }
],
"version": 0,
"behaviors": { "stackMultiMark": false },
"configType": "cartesian",
"_dependentAxis": "y"
}
},
{
"name": "Revenue by Status",
"topicName": "order_items",
"prefersChart": true,
"visType": "basic",
"chartType": "barGrouped",
"fields": ["order_items.status", "order_items.total_revenue"],
"query": {
"table": "order_items",
"fields": [
"order_items.status",
"order_items.total_revenue"
],
"sorts": [
{ "column_name": "order_items.total_revenue", "sort_descending": true }
],
"limit": 10,
"join_paths_from_topic_name": "order_items",
"visConfig": { "chartType": "barColor" }
},
"config": {
"y": { "field": { "name": "order_items.status" } },
"mark": { "type": "bar" },
"color": { "_stack": "group" },
"series": [{ "field": { "name": "order_items.total_revenue" }, "xAxis": "x" }],
"tooltip": [
{ "field": { "name": "order_items.status" } },
{ "field": { "name": "order_items.total_revenue" } }
],
"version": 0,
"behaviors": { "stackMultiMark": false },
"configType": "cartesian",
"_dependentAxis": "x"
}
}
]
}'curl -L "$OMNI_BASE_URL/api/v1/documents" \
-H "Authorization: Bearer $OMNI_API_KEY"curl -L "$OMNI_BASE_URL/api/v1/documents/{documentId}" \
-H "Authorization: Bearer $OMNI_API_KEY"curl -L -X PATCH "$OMNI_BASE_URL/api/v1/documents/{documentId}" \
-H "Authorization: Bearer $OMNI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Q1 Revenue Report (Updated)",
"clearExistingDraft": true
}'curl -L -X DELETE "$OMNI_BASE_URL/api/v1/documents/{documentId}" \
-H "Authorization: Bearer $OMNI_API_KEY"curl -L -X PUT "$OMNI_BASE_URL/api/v1/documents/{documentId}/move" \
-H "Authorization: Bearer $OMNI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"folderPath": "/Marketing/Reports",
"scope": "organization"
}'curl -L -X POST "$OMNI_BASE_URL/api/v1/documents/{documentId}/duplicate" \
-H "Authorization: Bearer $OMNI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Copy of Q1 Revenue Report",
"folderPath": "/Marketing/Reports"
}'curl -L "$OMNI_BASE_URL/api/v1/documents/{documentId}" \
-H "Authorization: Bearer $OMNI_API_KEY"
# → response includes "workbook_id"curl -L -X POST "$OMNI_BASE_URL/api/unstable/models/{workbookId}/yaml" \
-H "Authorization: Bearer $OMNI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"fileName": "order_items.view",
"yaml": "views:\n order_items:\n dimensions:\n is_high_value:\n sql: \"${sale_price} > 100\"\n label: High Value Order\n measures:\n high_value_count:\n sql: \"${order_items.id}\"\n aggregate_type: count_distinct\n label: High Value Orders"
}'curl -L "$OMNI_BASE_URL/api/v1/dashboards/{dashboardId}/filters" \
-H "Authorization: Bearer $OMNI_API_KEY"curl -L -X POST "$OMNI_BASE_URL/api/v1/documents" \
-H "Authorization: Bearer $OMNI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"modelId": "your-model-id",
"name": "Filtered Dashboard",
"filterConfig": {
"date_filter": {
"type": "date",
"label": "Date Range",
"kind": "TIME_FOR_INTERVAL_DURATION",
"ui_type": "PAST",
"left_side": "6 months ago",
"right_side": "6 months"
},
"state_filter": {
"type": "string",
"label": "State",
"kind": "EQUALS",
"fieldName": "users.state",
"values": []
}
},
"filterOrder": ["date_filter", "state_filter"],
"queryPresentations": [...]
}'Dashboard: {OMNI_BASE_URL}/dashboards/{identifier}
Workbook: {OMNI_BASE_URL}/w/{identifier}# Start async download
curl -L -X POST "$OMNI_BASE_URL/api/v1/dashboards/{dashboardId}/download" \
-H "Authorization: Bearer $OMNI_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "format": "pdf" }'
# Poll job
curl -L "$OMNI_BASE_URL/api/v1/jobs/{jobId}/status" \
-H "Authorization: Bearer $OMNI_API_KEY"