npx skills add ...
npx skills add exploreomni/omni-agent-skills --skill omni-content-builder
Create, update, and manage Omni Analytics documents and dashboards programmatically — document lifecycle, drafts, tiles, visualizations, filters, controls, and layouts — using the Omni CLI. Use this skill whenever someone wants to build a dashboard, create a workbook or document, add tiles or charts, configure dashboard filters or controls, set up a KPI view, lay out or rearrange tiles and pages, edit a dashboard as a draft and publish it, change dashboard settings, rename/move/duplicate/delete a dashboard, modify dashboard-level model customizations like workbook-specific joins or fields, or build and edit an Omni app (custom HTML content in place of a dashboard) — including casual phrasings like "make a dashboard for…", "add a chart to…", or "clean up this dashboard's layout" that don't name the skill. For running a query or pulling metrics use omni-query; for adding a field to the shared model use omni-model-builder.
npx skills add exploreomni/omni-agent-skills --skill omni-content-builder
Create, update, and manage Omni documents, dashboards, and apps programmatically via the Omni CLI — document lifecycle, drafts, workbook models, filters, controls, and dashboard content.
Tip: Use
omni-model-explorerto understand available fields andomni-content-explorerto find existing dashboards to modify or learn from.
Documents are created and edited through the v2 documents API (omni documents v2-*) — an explicit envelope of queryPresentations, controls, containers, and settings, edited through a draft → publish flow. This is the only path for building, reading, or changing a document — never fall back to the v1 documents create/get commands (v1 put/update were removed in CLI 1.2.2). A few document-management operations (list, delete, move, duplicate, downloads) have no v2 form; see Commands below.
omni query run, check viz spec consistency, and verify the dashboard by reading the draft back and executing its queries before publishing.v2-get tile back into a patch unchanged — a GET returns the inner vis config flat, but a patch only persists it nested under config. Re-sending the flat shape — even for an unrelated edit like a rename — silently drops the vis config (KPI loses its number, charts lose mark/series, markdown goes blank). Always re-author the inner visConfig nested under config. See references/documents-v2.md.
v2-get/snapshot and patching it back is also a round-trip: the inner config is flat and will be dropped (renders "No chart available"). A "restore to how it was" still needs the config re-nested. And verify the specific tile you wrote — re-read it (v2-get) and run its query to confirm the config persisted; if a visual/preview tool is available, you can also request a screenshot to confirm the render — including reverts; don't assume a restore is safe.containers (and the order arrays) are full replacements. To change one tile, send just that key. To delete a tile, set its key to null AND remove it from order. When you send containers, send the complete layout tree with your edit applied.v2-create only lays out the first tile — the rest are stored but render nowhere until you author the full containers tree. See references/containers.md."1" on create merges over a server seed tile — some seed properties (e.g. automaticVis: true) can win over what you sent. Read the document back and re-patch tile "1" if its exact fields matter.query must include the full collection-field set — sorts, filters, calculations, column_totals, row_totals, fill_fields, pivots, userEditedSQL (empty values are fine) alongside table, fields, limit, join_paths_from_topic_name. Omitting the schema-required ones is a 400 with per-field errors. Do not include modelId or model_extension_id — the server anchors tiles to the document's workbook model and silently rewrites any value you send.userEditedSQL tile. First apply the topic-first reflex (see omni-query): map each query's intent to a topic. If none fits, stop and ask whether to model it — extend a topic or create a new one (on a branch via omni-model-builder, validated, merged only with confirmation). Only build a non-topic / raw-SQL tile after the user has explicitly chosen that path (declined modeling, or it's a genuine one-off / userEditedSQL is required). Non-topic + Access Boost is never the default for handed-over SQL — it requires that explicit decision. This is the easiest thing to get wrong when "build a dashboard" starts from raw SQL.userEditedSQL (raw-SQL tile) or a bare base-view query (no join_paths_from_topic_name) — it is invisible to Viewer / Restricted Querier roles by default. Before finalizing such content, ask the user whether the dashboard's audience includes Restricted Queriers or Viewers. If it does, advise (don't silently enable) Access Boost: it makes those tiles viewable on the dashboard (dashboard-only — not the underlying workbook) for chosen users/groups or the whole org — provided the org capability is enabled and the caller has Manager on the document. Because it loosens access controls, treat enabling it as a separate, explicitly-confirmed step: recommend the narrowest scope that satisfies the need (specific users/groups over org-wide) and get a clear go-ahead before it's applied. See the Raw-SQL tiles recipe in references/queryPresentations.md and omni-admin → Document Permissions (which carries the full confirmation checklist and the commands/prerequisite).--body silently wins over shorthand flags — if you pass --body, every promoted flag (--name, --summary, --branch-id, …) is ignored without warning. Put those fields inside the JSON body instead, or use flags alone with no --body.v2-get-draft <identifier> <draftIdentifier> and v2-patch-draft-by-identifier <identifier> <draftIdentifier>.workbookModelId (omni documents list-drafts <identifier>) each time you need it.map — a field/timeframe switcher's {"<tileKey>": false} excludes that tile, exactly like a filter's. See references/controls.md.automaticVis: false — otherwise the renderer auto-derives a chart and the tile is blank. See references/visConfig.md.filters, not controls. To caption a tile with a filter's current value (e.g. a date window), use {{filters.…summary}} — not {{controls.<id>.…}} (a filter is configured as a control but mustache routes it to filters; the controls namespace is for FIELD_SELECTION/picker/Top-N controls and renders empty for a filter). The filters key is context-dependent: view.field in a markdown-viz tile (resolved against the tile's own query, so one token works per-tile), the control id in a dashboard text tile. Full namespace/token map + scenarios in references/mustache.md.query.filters needs the object form — the relative-date shorthand ("last 6 months") throws a 500; send {type:"date", kind:"TIME_FOR_INTERVAL_DURATION", ui_type:"PAST", left_side, right_side}. See references/documents-v2.md.visConfig renders as "Item missing" — the server seeds new tiles with visConfig.visType: null + automaticVis: false, which draws nothing. Every tile needs either an explicit visConfig (e.g. the table recipe in references/visConfig.md) or automaticVis: true to auto-derive one. Layout is separate: a multi-tile v2-create lays out all tiles you pass in order, so "Item missing" is a vis-config gap, not a containers gap.visType, or prefersChart are misconfigured. If the user asks for a specific chart, include the complete chart-specific config from references/visConfig.md nested under visConfig.visConfig.config. Use chartType: "table" only as a deliberate table fallback, not for requested charts.pivots array is present (reported Omni bug). If boolean filters aren't applying, remove the pivot and test again.identifier not id — get a document's identifier from the v2-create/patch responses or omni documents list records.omni unstable documents-import to update an existing dashboard — import creates a new document and may drop newly-added tiles. Use the draft flow on the existing document.omni query run returns a server-side parsing error for a tile query filter, validate the unfiltered base query once. Do not save that broken filter into the tile. If a dashboard-level control can satisfy the request, use that path and verify by readback; otherwise leave the dashboard unchanged and report the blocker.omni documents discard-draft <identifier>) and report what was preserved — the published document was never touched.Auth: a profile authenticates with an API key or OAuth. If
whoami(or any call) returns 401, hand off — ask the user to run! omni config login <profile>(OAuth 2.1 browser flow; it blocks ~2 min on the browser). Don't runconfig loginyourself in a headless/CI session (no browser → timeout); on a local interactive machine you may. See theomni-api-conventionsrule for profile setup (omni config init --auth oauth) and discovering command and request-body shapes with--schema.
Tip: Use
-o jsonto force structured output for programmatic parsing, or-o humanfor readable tables. The default isauto(human in a TTY, JSON when piped).--compactstrips indentation for piping.
Build and edit documents with the documents v2-* commands — always. There is no situation where you reach back to the v1 documents create/get path to build, read, or change a document; the v2 draft flow covers all of it.
| Operation | Command |
|---|---|
| Create document | documents v2-create |
| Read document / draft state | documents v2-get / v2-get-draft |
| Edit document (tiles, controls, layout, settings, rename) | documents v2-patch-draft (+ v2-patch-draft-by-identifier) |
| Get the workbook model ID | documents list-drafts → workbookModelId (open a draft first) |
| Publish a draft | documents v2-publish-draft |
| Read or edit an app (HTML instead of a dashboard) | documents v2-*-app — alpha, CLI ≥ 1.2.2; see references/documents-v2.md |
A handful of document-management operations have no v2 form — they aren't alternatives to the v2 build path, just the only command for that job: documents list / list-drafts (find documents and drafts), documents discard-draft (abandon a draft), documents delete / move / duplicate (lifecycle), documents get-queries (extract a tile's runnable query for validation), dashboards download / download-status, and models yaml-create / validate (model writes).
Omni dashboards are built from documents. A document's v2 state is an envelope of four slices:
queryPresentations.data, keyed by record key ("1", "2", …); order is the tab order. Tile keys must be NUMERIC strings ("1", "42") — a descriptive key like "revenue_kpi" or "probe" is rejected with 400 … Invalid key in record. Track the tile↔meaning mapping in your build script, not in the key.controls (see references/controls.md). Unlike tile keys, control ids here ARE arbitrary strings ("date_filter", "granularity") — don't carry that freedom over to tile keys, or vice-versa.containers tree — a tile renders only where a container references it (see references/containers.md).workbookModelId on the document's draft record from documents list-drafts.A document is edited through drafts: v2-patch-draft creates a draft and applies your patch; the published document is untouched until v2-publish-draft. v2-get returns the current draft state if a draft exists, else the published state. Drafts can also be bound to a model branch (see references/branch-bound-drafts.md).
Build every tile's query on a topic whenever possible: set the query table to the topic's base view and pass join_paths_from_topic_name: <topic>, plus topicName: <topic> on the presentation (the presentation-level topicName is tile-specific — a standalone query has no equivalent). Joined-view fields then resolve through the topic's join map from the base view. For the full shape — how the join map reaches joined-view fields, the worked example, and verifying with omni models get-topic (base_view_name/join_via_map) — see omni-query's Build queries on a topic.
Access matters: a tile not built on a topic is not accessible to restricted queriers/viewers. A bare base-view query (or a raw-SQL userEditedSQL tile) still works — it traverses the global relationships file — but is restricted-access-invisible in a dashboard. Use it only when no topic fits and the audience isn't restricted, or enable Access Boost on the document so Viewer/Restricted Querier roles can see it (dashboard-only — not the underlying workbook). To author a raw-SQL tile and boost it end-to-end, see references/queryPresentations.md → Raw-SQL tiles; for the boost commands and the org-level prerequisite, see omni-admin → Document Permissions (add-permits with accessBoost, or update-permission-settings).
Authoring limit (distinct from the visibility above): if you are a Restricted Querier, you can't author a non-topic / raw-SQL tile at all — QUERY_TOPICS queries topics only — so every tile you build must be topic-based. The non-topic + Access-Boost path is only for a QUERY_FULL_MODEL author building for a restricted audience. (Check with whoami; see omni-admin → Model Roles & Caller Access.)
If no existing topic fits the request, don't just fall back to a base view (or raw SQL) — ask the user whether to extend an existing topic or create a new one, and build it on a branch only with their go-ahead. Don't silently convert un-modeled SQL into non-topic tiles. Use omni-query to choose/decide the topic and omni-model-builder to create or modify one (branch → validate → merge only on confirmation).
<model-id> is the shared model; the server mints a per-document workbook model.{identifier, name, description} — when you need the workbook model ID, open a draft and read its workbookModelId from omni documents list-drafts <identifier>.--folder-id omitted → the document lands in the creator's personal "My documents" (requires personal-content permission). Pass a folder ID to place it in a shared folder.Pass the full envelope via --body. Tiles are keyed — write tile "1" explicitly to replace the server's seed tile:
The rendering spec goes in
visConfig.visConfig.config(visType beside it, spec nested underconfig).chartTypeandfieldssit at the outervisConfiglevel. Misplaced spec keys are silently dropped on write — see the round-trip warning in Known Issues. See references/queryPresentations.md and references/visConfig.md for the structures and per-chart-type configs.
Key points:
"1", "2", … and must appear in order to be tabs; tiles also need a containers entry to render on the dashboard. A multi-tile create auto-lays-out only tile "1" — author containers for the rest (references/containers.md).prefersChart must be true to render a chart; set automaticVis: false when you author an explicit vis config.query needs the full collection-field set (see Known Issues) and no modelId.controls and settings slices can be included in the same create body — see Dashboard Filters & Controls.To learn the exact structure for a chart type, build a reference dashboard in the Omni UI and read it back with omni documents v2-get <identifier> — remembering that the inner config reads back flat and must be re-nested under config before reuse.
--summary is written to the document's history audit trail. (There is no clearExistingDraft in the v2 flow — if a draft already exists, patch it directly with v2-patch-draft-by-identifier, or discard it first.)
Soft-deletes the document (moves to Trash).
Use "null" as the folder path to move to root. --scope is optional — auto-computed from the destination folder.
Only published documents can be duplicated. Draft documents return 404.
Edits go through the draft flow — the published dashboard is untouched until you publish, so validation happens before anything goes live:
omni documents v2-get <identifier> > doc.json.queryPresentations.data + append it to order (+ a containers tile stack so it renders).config (never echo the flat GET shape back).null and remove it from order (and its stack from containers).containers is a full replacement — send the whole tree with your edit applied.omni documents v2-patch-draft <identifier> --body @patch.json (or --body - < patch.json) — capture draftIdentifier from the response. Include a summary in the body for the audit trail.
Prefer
--body @filefor these bodies: it needs no shell quoting, keeps the patch diffable, and validates the JSON client-side — a malformed patch fails immediately with the byte offset instead of a server-side400.
omni documents v2-get-draft <identifier> <draftIdentifier>, run the affected queries (see Validation Loops). Iterate with omni documents v2-patch-draft-by-identifier <identifier> <draftIdentifier> --body ….omni documents v2-publish-draft <identifier>. On failure or abandonment, omni documents discard-draft <identifier> cleans up without touching the published doc.Error map, merge-semantics details, and recipes are in references/updating-dashboards.md.
First decide where a new field belongs. Don't default to the lowest-friction path — choose the field's right home, gated by what your access actually allows.
Step 0 — check your access. Run
omni whoami whoami --model-id <sharedModelId>and readrolesByModel[<id>].permissions:
QUERY_FULL_MODELpresent (Querier / Modeler / Admin) → you can create a branch → prefer a shared-model branch for anything reusable. IfUPDATEis also present (Modeler / Admin) you can merge it yourself (with the creator's OK); if it's absent (Querier), open the branch and request a merge — a Querier can branch and modify but not promote.QUERY_FULL_MODELabsent but you can still create content (Restricted Querier — hasUSE_WORKBOOKS) → you can't branch → use the workbook model (extension); the write is authorized against the shared model's permissions, which is exactly the restricted-but-workbook-capable case. (A Viewer lacksUSE_WORKBOOKSand can't author a dashboard at all, so they never reach this decision — content creation presupposesUSE_WORKBOOKS.) Scope ceiling for a Restricted Querier: their workbook-model writes are limited to.viewextensions (new dimensions/measures on an existing view) + decoration edits — not.topic(topics/joins), themodel/relationshipsfiles, or access grants (those needQUERY_FULL_MODELon a branch). Keep the build view-scoped; planning a topic/join/grant change for a restricted querier 403s mid-build and can strand the doc. (Canonical:omni-admin→ Model Roles & Caller Access.)The permission→capability map behind these signals (and why a shared-model-scoped
whoamidoesn't list branch ability as its own entry) is the canonical access check inomni-admin→ Model Roles & Caller Access — this skill just applies it. In short:QUERY_FULL_MODEL= the proxy for "can branch";UPDATE= "can merge/promote to shared."Then place the field. In order:
- Can it be a calculation? A table calculation is scoped to a single query/tile (computed on the result set). Prefer one for logic local to one query — but lean to a model field (→ #2/#3) when (a) the query shape rules a calc out, or (b) you're building multiple queries at once and the same logic spans them and can be expressed as a dimension/measure. Window-shaped logic (running total, moving average, % change) should almost always stay a calc — it runs post-query on the result set, not in-warehouse; only reach for an in-warehouse field when the window must span rows outside the result set. (See
omni-query's table-calculation guidance.)- Reusable elsewhere and you can branch (
QUERY_FULL_MODEL)? Add it to a branch on the shared model and followomni-model-builderto create, validate, and ship it — merge it yourself only if you haveUPDATE, otherwise open a PR / request a merge.- One-off for this dashboard (and not a calculation) — or reusable but you can't branch (no
QUERY_FULL_MODEL)? Add it to the workbook model — see Building a tile that queries a workbook-model field below.- Unsure (or reusable + can-branch, but the creator may not want a shared-model change)? Ask the creator where the field should live.
- Never write to the schema model — it's auto-generated and read-only.
If the field isn't in the published shared model yet — it lives only on a model branch that hasn't merged — put the tile on a branch-bound draft. See references/branch-bound-drafts.md.
Push custom dimensions and measures to a specific dashboard by writing to its workbook model. Each workbook has its own model that extends the shared model — so the ID you write YAML to is a model ID, not a separate "workbook ID". Because every edit goes through a draft, and the field has to exist before a tile can reference it, the whole flow stays in the v2 draft path:
Step 1 — open a draft and read its workbook model ID:
Note: The workbook model (which extends the shared model) is what you pass to
omni models yaml-create. Each draft has its own clone of it, and the ID changes on every draft → publish cycle — always read it fresh fromlist-drafts; never reuse a cached value.
Step 2 — POST YAML to the draft's workbook model with mode: "extension":
Critical: Always pass
"mode": "extension"when editing an existing view in a workbook model. The default is"combined", which treats your YAML body as the complete view definition and marks every field you didn't include asignored: true— silently breaking queries that depend on fields from the shared base view. Extension mode layers your new dimensions and measures on top of the inherited view.
fileName must be "model", "relationships", or end with .view or .topic. The yaml value is a YAML string (not a JSON object) containing the view's contents — no views: wrapper. Writing to a workbook model skips git sync entirely — authorization is still checked against the underlying shared model's permissions.
The YAML you write here is ordinary model YAML — don't improvise it. For the exact shape of dimensions, measures (incl. filtered measures like a count over a boolean flag), aggregate types, formats, and topics, follow
omni-model-builder→references/modelParameters.md, and for filter conditionsreferences/yaml-filter-syntax.md. A filtered measure'sfilters:value is always an operator object ({ is: true },{ greater_than: 100 }), never a bare scalar —is_flag: trueandstatus: "complete"are rejected as invalid filter specs.
v2 tile queries carry no modelId — the server anchors each tile to the draft's workbook model, so the field just has to exist in that model before the tile references it. The order is fixed:
documents v2-create (or use the existing document) — provisions the workbook model.documents v2-patch-draft <identifier> — open the draft (a --summary-only patch is enough to create it).documents list-drafts <identifier> → the draft's workbookModelId.models yaml-create <draftWorkbookModelId> with mode: "extension" → add the field (above).documents v2-patch-draft-by-identifier <identifier> <draftIdentifier> adding the tile that references the field — no modelId anywhere in the tile query.documents v2-publish-draft <identifier> — the field and tile go live together.After publishing, the workbook model ID has changed — open a new draft and re-read workbookModelId from list-drafts before any further yaml-create.
After writing, confirm the base view's fields are still available by querying one against the draft's workbook model:
(Standalone query run bodies still take a modelId — only tile queries inside v2 documents omit it.) If the response errors on a field that exists in the shared model (e.g. order_items.id), your write likely used combined mode and ignored the inherited fields. Re-run Step 2 with "mode": "extension".
Filters and interactive controls share one envelope slice: controls: {data, order}. Each entry is {config, map} — config holds the filter/control definition, map optionally scopes it per tile. Include them at create time or patch them in later (controls merge by key like tiles):
controls.data are arbitrary IDs and must match order.map scopes a control per tile: {"<tileKey>": false} excludes a tile, {"<tileKey>": "<fieldName>"} remaps it — for both filters and interactive switchers.fieldName with the fully qualified field name (no timeframe bracket for date filters), or it won't bind to any column.omni documents v2-get — the controls slice is directly reusable in a patch.settings is a shallow-merged object: crossfilterEnabled (click a value in one tile to filter the others), facetFilters, refreshInterval (seconds, null disables), runQueriesOn ("current-page" / "all-pages" / null), and customText ({queryError, queryNoResults} overrides). Patch only the keys you're changing.
The containers tree decides where tiles and controls render: a reserved "filter-bar" stack, then one page container per page, each holding a 24-column grid of tile stacks with gridPosition {x,y,w,h}. Safe default on create: omit containers and let the server lay out tile "1", then author the full tree when you add more tiles. When editing, containers is a full replacement — round-trip the existing array with your change applied. Multi-page dashboards, page switchers, grouped bands, in-tile controls, and sizing rules: references/containers.md.
After creating or finding content, always provide the user a direct link:
The identifier comes from the v2-create/patch responses or omni documents list; the draftIdentifier comes from the v2-patch-draft response or omni documents list-drafts.
Replace {OMNI_BASE_URL} with the actual base URL from the active profile or
environment, normalized without a trailing slash. Do not return the literal
placeholder string unless credentials are unavailable and you explicitly say the
URL is a template.
Every dashboard build or update must be validated before publishing — broken tiles, bad field references, and misconfigured viz specs fail silently ("Chart unavailable" / "No data") with no API-level error. The full methodology — commands, the viz-spec consistency table, and the post-creation checklist — is in references/validation-and-testing.md. In brief:
omni models validate <modelId>; treat any is_warning: false issue as an error.omni query run before building (the single most important step). Check for no error, cache_metadata.num_rows > 0 (the row count; summary.row_count doesn't exist), and include the same filters you'll use on the dashboard.prefersChart: true; spec nested in visConfig.visConfig.config; a valid chartType with matching visType/configType; correct _dependentAxis; the stack/color dimension in query.pivots. See references/visConfig.md.v2-get-draft, confirm queryPresentations.data keys match order, run omni documents get-queries + omni query run per tile, and report each tile's status + row count. After one failed corrected patch, discard the draft and report the blocker.omni-model-explorer to find topic + fieldsomni models validate <modelId> and check for errorsomni query run (using omni-query) before building the dashboard. Include the same filters you plan to use as controls to confirm they parse correctly. This catches field name typos, missing join paths, bad filter expressions, and permission errors before they become broken tiles.chartType/visType/inner config/prefersChart against the consistency rules before assembling the payloadomni documents v2-create with queryPresentations + controls + settings (and containers if multi-tile) in one bodyomni documents v2-get, confirm all tiles are present and placed, then run each tile's query via omni documents get-queries + omni query run to verify no broken tiles{OMNI_BASE_URL}/dashboards/{identifier} to the user (only after verification passes)omni-content-explorer or omni documents list to locate itomni documents v2-get <identifier>queryPresentations/controls/settings; full containers tree if layout changes; re-author inner vis configs nested under configomni query run; check viz specs against the consistency rulesomni documents v2-patch-draft <identifier> --body … (with a summary)v2-get-draft, then get-queries + query run on modified tilesomni documents v2-publish-draft <identifier>; on failure, discard-draft and report{OMNI_BASE_URL}/dashboards/{identifier} to the user (only after verification passes)omni-model-builder for shared fields, or the workbook-model flow above for dashboard-specific fieldsv2-get, update model fields, extract queries for reuseAsync render → poll → fetch. Poll the status field (in_progress → terminal error, or a success state carrying the result/URL). Early-exit on any terminal state — a fixed-count for … sleep loop that only breaks on success runs to the end if the filter is wrong or the render errors:
"Job failed to render."has two causes — disambiguate, don't assume. It's either (a) one tile in your dashboard throws at render (a markdown component handed an undefined value, a bad token) — a single bad tile fails the whole export — or (b) the render service is down. Tell them apart by exporting a known-good dashboard: if that renders, the fault is your content (find the bad tile — isolate by exporting a one-tile test doc, or binary-search tiles); if the known-good one also errors, the service is down — bound your attempts and report. Either way, while the render path is unavailable you can't PNG-verify markdown/CSS tiles, so fall back to draft read-back + per-tilequery run+ the UI. On success, fetch the image withomni dashboards download-file <dashboardId> <jobId>. Full catch-22 writeup: references/markdown-tiles.md.
Add queryIdentifierMapKey = the tile's queryPresentations.data key (e.g. "5") to the download body; it renders only that tile. Same async flow (download → download-status → download-file).
Format matrix: pdf / png / xlsx / csv / json — json is single-tile only (requires queryIdentifierMapKey), and for a full dashboard csv returns a ZIP of one CSV per tile. pdf/png give the image; csv/xlsx/json give the tile's data. For the authoritative paperFormat values and the full PDF/PNG + data option set (paperOrientation, hideTitle, showFilters, formatting/row-limit flags, filename, filterConfig, …), run omni dashboards download --schema — don't rely on this list staying exhaustive.
POST /v1/dashboards/{id}/download has no pages/pageId parameter. PNG is inherently one page, and a PDF of a multi-page dashboard returns only the default/active page — page selection exists only in the UI download form, not this endpoint (and not the schedules/deliveries API). There is no robust public-API way to render a chosen page today — tracked as an upstream API gap.
# Show available profiles and select the appropriate one
omni config show
# If multiple profiles exist, ask the user which to use, then switch:
omni config use <profile-name>
# Confirm the active profile is authenticated and inspect your permissions:
omni whoami whoamiomni documents --help # Document operations (v2-* + lifecycle)
omni dashboards --help # Dashboard downloads
omni models yaml-create --help # Writing model YAML
omni documents v2-create --schema # Body schema + example (add --depth 1 for an overview, --field PATH to drill in){
"name": "…", "description": "…",
"queryPresentations": { "data": { "<tileKey>": { /* tile: query + vis */ } }, "order": ["1", "2"] },
"controls": { "data": { "<controlId>": { "config": {…}, "map": {…} } }, "order": ["…"] },
"containers": [ /* layout tree: filter bar, pages, grids, tile stacks */ ],
"settings": { "crossfilterEnabled": …, "facetFilters": …, "refreshInterval": …, "runQueriesOn": …, "customText": … }
}omni documents v2-create <model-id> "Q1 Revenue Report"
# optional flags: --identifier, --description, --folder-idomni documents v2-create --body '{
"modelId": "your-shared-model-id",
"name": "Q1 Revenue Report",
"queryPresentations": {
"data": {
"1": {
"name": "Monthly Revenue Trend",
"type": "query",
"topicName": "order_items",
"prefersChart": true,
"automaticVis": false,
"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",
"calculations": [], "column_totals": {}, "row_totals": {},
"fill_fields": [], "pivots": [], "userEditedSQL": ""
},
"visConfig": {
"chartType": "lineColor",
"fields": ["order_items.created_at[month]", "order_items.total_revenue"],
"version": 0,
"visConfig": {
"visType": "basic",
"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" } }
],
"configType": "cartesian",
"_dependentAxis": "y"
}
}
}
}
},
"order": ["1"]
}
}'omni documents v2-patch-draft <identifier> --name "Q1 Revenue Report (Updated)" --summary "rename"
omni documents v2-publish-draft <identifier>omni documents delete <identifier>omni documents move <identifier> "/Marketing/Reports" --scope organizationomni documents duplicate <identifier> "Copy of Q1 Revenue Report" --folder-path "/Marketing/Reports"omni documents v2-patch-draft <identifier> --summary "add workbook field" # creates the draft
omni documents list-drafts <identifier>
# → use the draft record's "workbookModelId" — that IS the model you write YAML toomni models yaml-create <draftWorkbookModelId> --body '{
"fileName": "order_items.view",
"yaml": "dimensions:\n is_high_value:\n sql: \"${sale_price} > 100\"\n label: High Value Order\nmeasures:\n high_value_count:\n sql: \"${order_items.id}\"\n aggregate_type: count_distinct\n label: High Value Orders",
"mode": "extension"
}'omni query run --body '{
"query": {
"modelId": "<draftWorkbookModelId>",
"table": "order_items",
"fields": ["order_items.id", "order_items.high_value_count"],
"limit": 1,
"join_paths_from_topic_name": "order_items"
}
}'omni documents v2-create --body '{
"modelId": "your-shared-model-id",
"name": "Filtered Dashboard",
"controls": {
"data": {
"date_filter": {
"config": {
"type": "date", "kind": "TIME_FOR_INTERVAL_DURATION", "ui_type": "PAST",
"left_side": "6 months ago", "right_side": "6 months",
"fieldName": "order_items.created_at",
"topic": "order_items", "base_view": "order_items",
"label": "Date Range"
},
"map": {}
},
"state_filter": {
"config": {
"type": "string", "kind": "EQUALS",
"fieldName": "users.state",
"topic": "order_items", "base_view": "order_items",
"label": "State", "values": []
},
"map": {}
}
},
"order": ["date_filter", "state_filter"]
},
"queryPresentations": { … }
}'Dashboard: {OMNI_BASE_URL}/dashboards/{identifier}
Workbook: {OMNI_BASE_URL}/w/{identifier}
Draft: {OMNI_BASE_URL}/dashboards/{draftIdentifier}# Start async download
JID=$(omni dashboards download <dashboardId> --body '{ "format": "png" }' -o json \
| python3 -c 'import json,sys;print(json.load(sys.stdin)["job_id"])')
# Poll with early exit: anything that is NOT still-in-progress is terminal → break and inspect
while :; do
R=$(omni dashboards download-status <dashboardId> "$JID" -o json)
S=$(echo "$R" | python3 -c 'import json,sys;print((json.load(sys.stdin).get("status") or "").lower())')
case "$S" in
in_progress|queued|pending|running) sleep 4 ;;
*) echo "$R"; break ;; # terminal: success carries the result/URL; "error" carries .error
esac
doneomni dashboards download <dashboardId> --body '{ "format": "png", "queryIdentifierMapKey": "5" }'