diff --git a/api/openapi.json b/api/openapi.json index 98db825..17b7e2e 100644 --- a/api/openapi.json +++ b/api/openapi.json @@ -1289,6 +1289,81 @@ "state" ] }, + "AiJobFeedbackResponse": { + "type": "object", + "properties": { + "comment": { + "type": [ + "string", + "null" + ], + "description": "The comment as recorded, or null when none was sent.", + "example": "Grouped by order date instead of ship date." + }, + "conversationId": { + "type": "string", + "format": "uuid", + "description": "The conversation the rated job belongs to. Analytics keys feedback by job id and conversation id.", + "example": "660e8400-e29b-41d4-a716-446655440001" + }, + "jobId": { + "type": "string", + "format": "uuid", + "description": "The job whose response was rated.", + "example": "550e8400-e29b-41d4-a716-446655440000" + }, + "rating": { + "type": "string", + "enum": [ + "good", + "bad" + ], + "description": "The verdict on the response. `good` is a thumbs up, `bad` a thumbs down — the same signal as the buttons under a response in the Omni chat UI.", + "example": "bad" + }, + "submittedAt": { + "type": "string", + "format": "date-time", + "description": "When the feedback was recorded.", + "example": "2025-01-15T10:00:00.000Z" + } + }, + "required": [ + "comment", + "conversationId", + "jobId", + "rating", + "submittedAt" + ] + }, + "AiJobFeedbackBody": { + "type": "object", + "properties": { + "comment": { + "type": [ + "string", + "null" + ], + "minLength": 1, + "maxLength": 5000, + "description": "Free-text feedback about the response — what was wrong, or what an automated evaluation found. Whitespace is trimmed; null is treated as no comment.", + "example": "Grouped by order date instead of ship date." + }, + "rating": { + "type": "string", + "enum": [ + "good", + "bad" + ], + "description": "The verdict on the response. `good` is a thumbs up, `bad` a thumbs down — the same signal as the buttons under a response in the Omni chat UI.", + "example": "bad" + } + }, + "required": [ + "rating" + ], + "additionalProperties": false + }, "AiJobResultResponse": { "type": "object", "properties": { @@ -2154,9 +2229,9 @@ "records": { "type": "array", "items": { - "$ref": "#/components/schemas/RoutineResponse" + "$ref": "#/components/schemas/RoutineSummaryResponse" }, - "description": "Routines returned for this request, newest first." + "description": "Routine summaries returned in the requested order, newest first by default. Condition queries are available from the item GET endpoint rather than repeated in list responses." } }, "required": [ @@ -2164,7 +2239,7 @@ "records" ] }, - "RoutineResponse": { + "RoutineSummaryResponse": { "type": "object", "properties": { "branchId": { @@ -2284,8 +2359,8 @@ "string", "null" ], - "description": "The delivery condition identified in the routine's current prompt, in plain language. Null on routines created before Omni stored delivery conditions.", - "example": "total signups dropped more than 20% versus the prior week" + "description": "The routine's delivery condition, in plain language. Null for routines created before Omni stored delivery-condition text.", + "example": "there are failed orders" }, "conditionType": { "type": "string", @@ -2295,7 +2370,7 @@ "RESULTS_PRESENT", "RESULTS_MISSING" ], - "description": "How the condition query gates delivery. RESULTS_PRESENT fires when the condition query returns rows, RESULTS_MISSING when it returns none, RESULTS_CHANGED when its results differ from the previous scheduled run, RESULTS_UNCHANGED when they are identical.", + "description": "How Omni evaluates the condition. RESULTS_PRESENT delivers when the condition query returns rows, RESULTS_MISSING when it returns none, RESULTS_CHANGED when its results differ from the previous scheduled run, and RESULTS_UNCHANGED when they are identical.", "example": "RESULTS_PRESENT" } }, @@ -2303,7 +2378,7 @@ "conditionPrompt", "conditionType" ], - "description": "The condition gating delivery, or null for a routine that delivers on every scheduled run." + "description": "The delivery condition, or null for a routine that delivers on every scheduled run." }, "RoutineDestinationResponse": { "oneOf": [ @@ -2433,6 +2508,29 @@ "RoutineCreateResponse": { "type": "object", "properties": { + "condition": { + "allOf": [ + { + "$ref": "#/components/schemas/RoutineCondition" + }, + { + "type": "object", + "properties": { + "query": { + "$ref": "#/components/schemas/RoutineConditionQuery" + }, + "verification": { + "$ref": "#/components/schemas/RoutineConditionVerification" + } + }, + "required": [ + "query", + "verification" + ] + } + ], + "description": "The generated condition, present only when the request supplied one." + }, "id": { "type": "string", "format": "uuid", @@ -2444,13 +2542,105 @@ "id" ] }, - "RoutineCreateApiError400": { + "RoutineConditionQuery": { + "type": "object", + "properties": { + "fields": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Ordered list of fully qualified field names to include in the query (e.g., \"view_name.field_name\").", + "example": [ + "products.name", + "order_items.total_revenue" + ] + }, + "filters": { + "type": "object", + "additionalProperties": {}, + "description": "Filter conditions keyed by fully qualified field name. Filter values vary by field type." + }, + "limit": { + "type": "integer", + "description": "Maximum number of rows to return.", + "example": 500 + }, + "sorts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AiQuerySort" + }, + "description": "Sort specifications applied to the query results." + }, + "table": { + "type": "string", + "description": "The base topic or view name for the query.", + "example": "order_items" + } + }, + "required": [ + "fields" + ], + "additionalProperties": {}, + "example": { + "aiGenerated": true, + "calculations": [], + "column_totals": {}, + "fields": [ + "orders.id" + ], + "fill_fields": [], + "filters": { + "orders.status": { + "case_insensitive": true, + "kind": "EQUALS", + "type": "string", + "values": [ + "failed" + ] + } + }, + "limit": 1, + "modelId": "770e8400-e29b-41d4-a716-446655440002", + "pivots": [], + "row_totals": {}, + "sorts": [], + "table": "orders", + "userEditedSQL": "SELECT ${orders.id}\nFROM ${orders}\nWHERE UPPER(${orders.status}) = UPPER('failed')\nLIMIT 1", + "version": 9 + }, + "description": "The generated semantic query Omni stores and evaluates for this condition. This can be inspected or passed to the query API." + }, + "RoutineConditionVerification": { + "type": [ + "object", + "null" + ], + "properties": { + "conditionMetNow": { + "type": "boolean", + "description": "Whether the condition held when Omni verified it. Present only for RESULTS_PRESENT and RESULTS_MISSING.", + "example": false + }, + "rowCount": { + "type": "integer", + "description": "Rows the condition query returned when Omni verified it.", + "example": 0 + } + }, + "required": [ + "rowCount" + ], + "description": "What Omni observed when it ran the condition query during creation, or null when condition.verify was false." + }, + "RoutineConditionApiError400": { "type": "object", "properties": { "detail": { "type": "string", "description": "Human-readable error message describing what went wrong.", - "example": "The prompt does not state when the routine should deliver. Add a specific, measurable delivery condition, then try again." + "example": "The delivery condition is too vague or subjective to evaluate. State a specific, measurable condition, then try again." }, "status": { "type": "integer", @@ -2463,11 +2653,10 @@ "not_expressible", "no_query", "malformed_query", - "dry_run_failed", - "no_condition_detected" + "dry_run_failed" ], - "description": "A machine-readable error code returned when Omni cannot create or validate a delivery condition. This field is absent for other validation errors.", - "example": "no_condition_detected" + "description": "A machine-readable error code returned when Omni cannot create, replace, or validate a delivery condition. This field is absent for other validation errors.", + "example": "not_expressible" } }, "required": [ @@ -2494,6 +2683,25 @@ "status" ] }, + "RoutineConditionApiError503": { + "type": "object", + "properties": { + "detail": { + "type": "string", + "description": "Human-readable error message describing what went wrong.", + "example": "Omni was temporarily unable to compose or verify the delivery condition. Try again shortly." + }, + "status": { + "type": "integer", + "description": "HTTP status code of the error.", + "example": 503 + } + }, + "required": [ + "detail", + "status" + ] + }, "RoutineCreateBody": { "type": "object", "properties": { @@ -2509,16 +2717,6 @@ "description": "Optional human-readable notes about the routine. Display-only — never used as model input.", "example": "Weekly signups summary for the growth team." }, - "gating": { - "type": "string", - "enum": [ - "auto", - "alert", - "unconditional" - ], - "description": "Deprecated. Delivery conditions are no longer derived from the prompt. 'auto' (the default) and 'unconditional' create a routine that delivers on every scheduled run. 'alert' declares a delivery condition Omni cannot honor from the prompt alone and fails with code no_condition_detected.", - "example": "unconditional" - }, "modelId": { "type": "string", "format": "uuid", @@ -2556,6 +2754,9 @@ "description": "Topic name to scope query generation. If omitted, the AI picks the best topic.", "example": "users" }, + "condition": { + "$ref": "#/components/schemas/RoutineConditionInput" + }, "destination": { "$ref": "#/components/schemas/RoutineDestination" } @@ -2570,6 +2771,29 @@ ], "additionalProperties": false }, + "RoutineConditionInput": { + "type": "object", + "properties": { + "conditionPrompt": { + "type": "string", + "minLength": 1, + "maxLength": 2000, + "description": "The delivery condition in plain language. Omni composes a semantic query from this text.", + "example": "there are failed orders" + }, + "verify": { + "type": "boolean", + "default": true, + "description": "Whether Omni should run the generated condition query before saving. Defaults to true. Verification confirms that the query runs against the warehouse and establishes the comparison baseline. Set false to save without running the query; the next scheduled evaluation then establishes the baseline for change-based conditions.", + "example": true + } + }, + "required": [ + "conditionPrompt" + ], + "additionalProperties": false, + "description": "Delivers only when this condition holds. Omni composes a semantic query from the text and, by default, verifies it before creating the routine. Omit for a routine that delivers on every scheduled run." + }, "RoutineDestination": { "oneOf": [ { @@ -2632,9 +2856,361 @@ ], "additionalProperties": false }, + "RoutineResponse": { + "type": "object", + "properties": { + "branchId": { + "type": [ + "string", + "null" + ], + "format": "uuid", + "description": "Branch of the shared model the prompt runs against, or null." + }, + "condition": { + "$ref": "#/components/schemas/RoutineConditionDetail" + }, + "createdAt": { + "type": "string", + "description": "ISO 8601 timestamp when the routine was created." + }, + "description": { + "type": [ + "string", + "null" + ], + "description": "Display-only notes about the routine, or null." + }, + "destination": { + "$ref": "#/components/schemas/RoutineDestinationResponse" + }, + "disabled": { + "type": "boolean", + "description": "Whether the owner has paused the routine." + }, + "id": { + "type": "string", + "format": "uuid", + "description": "The unique identifier of the routine." + }, + "lastRun": { + "$ref": "#/components/schemas/RoutineLastRun" + }, + "modelId": { + "type": "string", + "format": "uuid", + "description": "The model the prompt runs against." + }, + "name": { + "type": "string", + "description": "Customer-visible name of the routine. Used as the email subject for email destinations, and shown on Slack deliveries." + }, + "prompt": { + "type": "string", + "description": "Natural language prompt Omni runs on each scheduled run." + }, + "recipientCount": { + "type": "integer", + "description": "Number of distinct deliverable recipients. For email, user groups are expanded to members and duplicates removed; a Slack routine is always 1 (its single channel or DM)." + }, + "schedule": { + "type": "string", + "description": "Six-field cron expression (minute, hour, day-of-month, month, day-of-week, year; use `?` for an unspecified day field)." + }, + "systemDisabled": { + "type": "boolean", + "description": "Whether Omni disabled the routine because it could no longer run successfully or safely." + }, + "systemDisabledReason": { + "type": [ + "string", + "null" + ], + "description": "Reason Omni disabled the routine, or null." + }, + "timezone": { + "type": "string", + "description": "IANA timezone identifier used to evaluate the schedule." + }, + "topicName": { + "type": [ + "string", + "null" + ], + "description": "Topic scoping query generation, or null." + }, + "updatedAt": { + "type": "string", + "description": "ISO 8601 timestamp when the routine was last updated." + } + }, + "required": [ + "branchId", + "condition", + "createdAt", + "description", + "destination", + "disabled", + "id", + "lastRun", + "modelId", + "name", + "prompt", + "recipientCount", + "schedule", + "systemDisabled", + "systemDisabledReason", + "timezone", + "topicName", + "updatedAt" + ] + }, + "RoutineConditionDetail": { + "type": [ + "object", + "null" + ], + "properties": { + "conditionPrompt": { + "type": [ + "string", + "null" + ], + "description": "The routine's delivery condition, in plain language. Null for routines created before Omni stored delivery-condition text.", + "example": "there are failed orders" + }, + "conditionType": { + "type": "string", + "enum": [ + "RESULTS_CHANGED", + "RESULTS_UNCHANGED", + "RESULTS_PRESENT", + "RESULTS_MISSING" + ], + "description": "How Omni evaluates the condition. RESULTS_PRESENT delivers when the condition query returns rows, RESULTS_MISSING when it returns none, RESULTS_CHANGED when its results differ from the previous scheduled run, and RESULTS_UNCHANGED when they are identical.", + "example": "RESULTS_PRESENT" + }, + "query": { + "allOf": [ + { + "$ref": "#/components/schemas/RoutineConditionQuery" + }, + { + "description": "The persisted semantic query Omni evaluates for this condition. This can be inspected or passed to the query API." + } + ] + } + }, + "required": [ + "conditionPrompt", + "conditionType", + "query" + ], + "description": "The delivery condition and its persisted semantic query, or null for a routine that delivers on every scheduled run." + }, + "RoutineUpdateResponse": { + "type": "object", + "properties": { + "branchId": { + "type": [ + "string", + "null" + ], + "format": "uuid", + "description": "Branch of the shared model the prompt runs against, or null." + }, + "condition": { + "$ref": "#/components/schemas/RoutineConditionUpdateResponse" + }, + "createdAt": { + "type": "string", + "description": "ISO 8601 timestamp when the routine was created." + }, + "description": { + "type": [ + "string", + "null" + ], + "description": "Display-only notes about the routine, or null." + }, + "destination": { + "$ref": "#/components/schemas/RoutineDestinationResponse" + }, + "disabled": { + "type": "boolean", + "description": "Whether the owner has paused the routine." + }, + "id": { + "type": "string", + "format": "uuid", + "description": "The unique identifier of the routine." + }, + "lastRun": { + "$ref": "#/components/schemas/RoutineLastRun" + }, + "modelId": { + "type": "string", + "format": "uuid", + "description": "The model the prompt runs against." + }, + "name": { + "type": "string", + "description": "Customer-visible name of the routine. Used as the email subject for email destinations, and shown on Slack deliveries." + }, + "prompt": { + "type": "string", + "description": "Natural language prompt Omni runs on each scheduled run." + }, + "recipientCount": { + "type": "integer", + "description": "Number of distinct deliverable recipients. For email, user groups are expanded to members and duplicates removed; a Slack routine is always 1 (its single channel or DM)." + }, + "schedule": { + "type": "string", + "description": "Six-field cron expression (minute, hour, day-of-month, month, day-of-week, year; use `?` for an unspecified day field)." + }, + "systemDisabled": { + "type": "boolean", + "description": "Whether Omni disabled the routine because it could no longer run successfully or safely." + }, + "systemDisabledReason": { + "type": [ + "string", + "null" + ], + "description": "Reason Omni disabled the routine, or null." + }, + "timezone": { + "type": "string", + "description": "IANA timezone identifier used to evaluate the schedule." + }, + "topicName": { + "type": [ + "string", + "null" + ], + "description": "Topic scoping query generation, or null." + }, + "updatedAt": { + "type": "string", + "description": "ISO 8601 timestamp when the routine was last updated." + } + }, + "required": [ + "branchId", + "condition", + "createdAt", + "description", + "destination", + "disabled", + "id", + "lastRun", + "modelId", + "name", + "prompt", + "recipientCount", + "schedule", + "systemDisabled", + "systemDisabledReason", + "timezone", + "topicName", + "updatedAt" + ] + }, + "RoutineConditionUpdateResponse": { + "type": [ + "object", + "null" + ], + "properties": { + "conditionPrompt": { + "type": [ + "string", + "null" + ], + "description": "The routine's delivery condition, in plain language. Null for routines created before Omni stored delivery-condition text.", + "example": "there are failed orders" + }, + "conditionType": { + "type": "string", + "enum": [ + "RESULTS_CHANGED", + "RESULTS_UNCHANGED", + "RESULTS_PRESENT", + "RESULTS_MISSING" + ], + "description": "How Omni evaluates the condition. RESULTS_PRESENT delivers when the condition query returns rows, RESULTS_MISSING when it returns none, RESULTS_CHANGED when its results differ from the previous scheduled run, and RESULTS_UNCHANGED when they are identical.", + "example": "RESULTS_PRESENT" + }, + "query": { + "allOf": [ + { + "$ref": "#/components/schemas/RoutineConditionQuery" + }, + { + "description": "The persisted semantic query Omni evaluates for this condition. This can be inspected or passed to the query API." + } + ] + }, + "verification": { + "type": [ + "object", + "null" + ], + "properties": { + "conditionMetNow": { + "type": "boolean", + "description": "Whether the condition held when Omni verified it. Present only for RESULTS_PRESENT and RESULTS_MISSING.", + "example": false + }, + "rowCount": { + "type": "integer", + "description": "Rows the condition query returned when Omni verified it.", + "example": 0 + } + }, + "required": [ + "rowCount" + ], + "description": "What Omni observed while verifying the replacement condition, null when condition.verify was false, and absent when the request did not derive a condition." + } + }, + "required": [ + "conditionPrompt", + "conditionType", + "query" + ], + "description": "The saved delivery condition and persisted query, or null for a routine that delivers on every scheduled run. A condition derived by this request also includes its verification result." + }, "RoutineUpdateBody": { "type": "object", "properties": { + "condition": { + "type": [ + "object", + "null" + ], + "properties": { + "conditionPrompt": { + "type": "string", + "minLength": 1, + "maxLength": 2000, + "description": "The delivery condition in plain language. Omni composes a semantic query from this text.", + "example": "there are failed orders" + }, + "verify": { + "type": "boolean", + "default": true, + "description": "Whether Omni should run the generated condition query before saving. Defaults to true. Verification confirms that the query runs against the warehouse and establishes the comparison baseline. Set false to save without running the query; the next scheduled evaluation then establishes the baseline for change-based conditions.", + "example": true + } + }, + "required": [ + "conditionPrompt" + ], + "additionalProperties": false, + "description": "Replaces the delivery condition: Omni recomposes the condition query, verifies it by default, resets the comparison baseline, and cancels any in-flight condition evaluation so a run started under the old condition cannot deliver. Pass null to remove the condition, making the routine deliver on every scheduled run. Omit to leave the stored condition unchanged. Resending the stored condition text unchanged is a no-op." + }, "description": { "type": [ "string", @@ -3782,11 +4358,16 @@ "description": "Compatible with pdf and png formats. If true, dashboard tiles will be arranged into a single vertical column.", "example": false }, - "useCache": { - "type": "boolean", - "default": false, - "description": "If true, allow scheduled queries to use cached results instead of always running fresh queries.", - "example": false + "cache": { + "type": "string", + "enum": [ + "Standard", + "SkipRequery", + "SkipCache" + ], + "default": "Standard", + "description": "Cache policy for the queries run by the download. Must be one of: Standard (standard caching behavior; default), SkipRequery (uses cached results if available, but does not requery), SkipCache (bypasses the cache and always executes fresh queries).", + "example": "SkipCache" }, "filename": { "type": "string", @@ -3794,6 +4375,11 @@ "maxLength": 255, "description": "Custom filename for the downloaded file (without extension)", "example": "monthly-report" + }, + "useCache": { + "type": "boolean", + "deprecated": true, + "description": "Deprecated and has no effect. Use `cache` instead." } }, "required": [ @@ -4345,279 +4931,6 @@ "refreshInterval" ] }, - "DocumentsPutResponse": { - "type": "object", - "properties": { - "description": { - "type": [ - "string", - "null" - ], - "description": "Document description" - }, - "identifier": { - "type": "string", - "description": "Document identifier" - }, - "name": { - "type": "string", - "description": "Updated document name" - } - }, - "required": [ - "identifier", - "name" - ] - }, - "DocumentsPutBody": { - "type": "object", - "properties": { - "clearExistingDraft": { - "type": "boolean", - "default": false, - "description": "Clear existing draft before updating (for published documents with drafts)" - }, - "description": { - "type": [ - "string", - "null" - ], - "description": "Document description" - }, - "documentMetadata": { - "description": "Document presentation metadata" - }, - "facetFilters": { - "type": "boolean", - "description": "Enable facet filters" - }, - "filterConfig": { - "description": "Filter configuration" - }, - "filterOrder": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Order of filters" - }, - "modelId": { - "type": "string", - "description": "Model ID" - }, - "name": { - "type": "string", - "minLength": 1, - "maxLength": 254, - "description": "Document name" - }, - "queryPresentations": { - "type": "array", - "items": { - "$ref": "#/components/schemas/DocumentsPutQueryPresentation" - }, - "minItems": 1, - "description": "Query presentations (full replacement)" - }, - "refreshInterval": { - "type": [ - "integer", - "null" - ], - "minimum": 60, - "description": "Auto-refresh interval in seconds" - } - }, - "required": [ - "facetFilters", - "filterOrder", - "modelId", - "name", - "queryPresentations", - "refreshInterval" - ] - }, - "DocumentsPutQueryPresentation": { - "type": "object", - "properties": { - "aiConfig": { - "type": "object", - "properties": { - "description": { - "type": "object", - "properties": { - "aiContext": { - "type": "string" - }, - "enabled": { - "type": "boolean" - } - } - }, - "subTitle": { - "type": "object", - "properties": { - "aiContext": { - "type": "string" - }, - "enabled": { - "type": "boolean" - } - } - } - }, - "description": "AI configuration" - }, - "chartType": { - "type": [ - "string", - "null" - ], - "enum": [ - "auto", - "area", - "areaStacked", - "areaStackedPercentage", - "bar", - "barLine", - "barGrouped", - "barStacked", - "barStackedPercentage", - "boxplot", - "code", - "column", - "columnGrouped", - "columnStacked", - "columnStackedPercentage", - "heatmap", - "kpi", - "line", - "lineColor", - "map", - "regionMap", - "markdown", - "omni-ai-summary-markdown", - "pie", - "funnel", - "sankey", - "point", - "pointColor", - "pointSize", - "pointSizeColor", - "singleRecord", - "omni-spreadsheet", - "summaryValue", - "svgMap", - "table", - "treemap", - null - ], - "description": "Chart type" - }, - "description": { - "type": "string", - "maxLength": 500, - "description": "Description" - }, - "name": { - "type": "string", - "minLength": 1, - "maxLength": 144, - "description": "Query presentation name" - }, - "prefersChart": { - "type": "boolean", - "description": "Whether to prefer chart view" - }, - "query": { - "description": "Query definition" - }, - "queryIdentifierMapKey": { - "type": "string", - "pattern": "^[1-9][0-9]*$", - "description": "Round-trip preservation hint. When the value matches an existing key on the document, the tile keeps its map key (and dashboard containers stay attached). Omit for new tiles. Must be a positive integer string (e.g. \"1\", \"2\", \"10\")." - }, - "resultConfig": { - "description": "Result config" - }, - "subTitle": { - "type": "string", - "maxLength": 250, - "description": "Subtitle" - }, - "topicName": { - "type": [ - "string", - "null" - ], - "maxLength": 256, - "description": "Topic name. Omit or pass null for raw-SQL tiles or any tile with no semantic topic." - }, - "visConfig": { - "$ref": "#/components/schemas/ApiVisConfig" - } - }, - "required": [ - "name" - ] - }, - "DocumentsUpdateResponse": { - "type": "object", - "properties": { - "description": { - "type": [ - "string", - "null" - ], - "description": "Document description" - }, - "identifier": { - "type": "string", - "description": "Document identifier" - }, - "name": { - "type": "string", - "description": "Updated document name" - } - }, - "required": [ - "identifier", - "name" - ] - }, - "DocumentsUpdateBody": { - "type": "object", - "properties": { - "clearExistingDraft": { - "type": "boolean", - "default": false, - "description": "Clear existing draft before updating (for published documents with drafts)" - }, - "description": { - "type": [ - "string", - "null" - ], - "description": "Document description" - }, - "identifier": { - "allOf": [ - { - "$ref": "#/components/schemas/DocumentIdentifier" - }, - { - "description": "New identifier for the document. Must be unique within the organization. The previous identifier is retained in the document identifier history and continues to redirect." - } - ] - }, - "name": { - "type": "string", - "minLength": 1, - "maxLength": 254, - "description": "New document name" - } - } - }, "SuccessResponse": { "type": "object", "properties": { @@ -4832,7 +5145,7 @@ }, "organizationAccessBoost": { "type": "boolean", - "description": "Boost organization access. Rejected with 403 when the organization has AccessBoost turned off." + "description": "Boost organization access. Rejected with 403 when the organization has AccessBoost turned off, or when a user-scoped key lacks boost provisioning." }, "organizationRole": { "type": "string", @@ -4856,7 +5169,7 @@ "accessBoost": { "type": "boolean", "default": false, - "description": "Grant access boost. Rejected with 403 when the organization has AccessBoost turned off." + "description": "Grant access boost. Rejected with 403 when the organization has AccessBoost turned off, or when a user-scoped key lacks boost provisioning." }, "role": { "type": "string", @@ -4897,7 +5210,7 @@ "properties": { "accessBoost": { "type": "boolean", - "description": "Access boost setting. Rejected with 403 when the organization has AccessBoost turned off." + "description": "Access boost setting. Rejected with 403 when the organization has AccessBoost turned off, or when a user-scoped key lacks boost provisioning." }, "role": { "type": "string", @@ -26538,6 +26851,9 @@ "DocumentsV2ReadResponse": { "type": "object", "properties": { + "app": { + "$ref": "#/components/schemas/DocumentsV2AppSlice" + }, "containers": { "$ref": "#/components/schemas/Containers" }, @@ -26581,6 +26897,20 @@ "workbookModelId" ] }, + "DocumentsV2AppSlice": { + "type": "object", + "properties": { + "url": { + "type": "string", + "description": "API path of the app sub-resource, where the app's HTML and settings are read and written. The sub-resource is an alpha surface: its shapes may change without a deprecation cycle while apps mature. The document routes are stable." + } + }, + "required": [ + "url" + ], + "additionalProperties": false, + "description": "Present only for app documents. The app's HTML and settings live at the `url` sub-resource; the document body carries nothing app-scoped." + }, "Containers": { "type": "array", "items": { @@ -30907,6 +31237,16 @@ } ] }, + "DocumentsV2AppEcho": { + "type": "object", + "properties": { + "url": { + "type": "string" + } + }, + "additionalProperties": false, + "description": "Accepted so a GET of an app document round-trips through PATCH; nothing in it is written. A `url` that does not address this document's app is rejected with 409. App content and settings are written only at the `…/app` sub-resource." + }, "DocumentsV2PatchDraftBody": { "type": "object", "properties": { @@ -30947,6 +31287,9 @@ "maxLength": 255, "description": "Optional. Caller-supplied description of what this patch changes, written to the history audit trail. When omitted, the server auto-generates one from the touched sections." }, + "app": { + "$ref": "#/components/schemas/DocumentsV2AppEcho" + }, "modelId": { "type": "string", "format": "uuid", @@ -30960,6 +31303,136 @@ }, "additionalProperties": false }, + "DocumentsV2AppContentResponse": { + "type": "object", + "properties": { + "html": { + "type": "string", + "description": "The complete app HTML document." + }, + "settings": { + "$ref": "#/components/schemas/DocumentsV2AppSettings" + } + }, + "required": [ + "html", + "settings" + ] + }, + "DocumentsV2AppSettings": { + "type": "object", + "properties": { + "allowClipboard": { + "type": "boolean", + "default": false + }, + "allowDefaultMapProviders": { + "type": "boolean", + "default": false + }, + "allowDownloads": { + "type": "boolean", + "default": false + }, + "allowExternalNavigation": { + "type": "boolean", + "default": false + }, + "allowInternalNavigation": { + "type": "boolean", + "default": false + }, + "externalNavOpensInNewTab": { + "type": "boolean", + "default": true + }, + "navAllowedDomains": { + "type": "array", + "items": { + "type": "string" + }, + "default": [] + }, + "navAllowedDomainsEnabled": { + "type": "boolean", + "default": false + }, + "safeDomains": { + "type": "array", + "items": { + "type": "string" + }, + "default": [] + }, + "safeDomainsEnabled": { + "type": "boolean", + "default": false + } + }, + "additionalProperties": false, + "description": "The app's sandbox settings: capability toggles plus the safe-domain and navigation allowlists. A write replaces the whole object; omitted fields take their locked-down defaults. Host lists are normalized (deduped, invalid or Omni-owned hosts dropped) before they are stored." + }, + "DocumentsV2PutAppResponse": { + "allOf": [ + { + "$ref": "#/components/schemas/DocumentsV2PatchDraftResponse" + }, + { + "type": "object", + "properties": { + "app": { + "$ref": "#/components/schemas/DocumentsV2AppState" + }, + "warnings": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Non-blocking warnings — present only when there are any. Currently: external resource hosts the app's iframe CSP will block until an org admin allows them. The write itself succeeded." + } + }, + "required": [ + "app" + ] + } + ] + }, + "DocumentsV2AppState": { + "type": "object", + "properties": { + "settings": { + "$ref": "#/components/schemas/DocumentsV2AppSettings" + } + }, + "required": [ + "settings" + ] + }, + "DocumentsV2PutAppBody": { + "type": "object", + "properties": { + "html": { + "type": "string", + "minLength": 1, + "maxLength": 2097152, + "description": "The complete app HTML document. Replaces the current HTML; capped at 2 MiB of UTF-8 (maxLength counts characters — the byte cap is what the server enforces). Every write appends an immutable revision." + }, + "settings": { + "allOf": [ + { + "$ref": "#/components/schemas/DocumentsV2AppSettings" + }, + { + "description": "When present, replaces the app settings; omitted fields take their locked-down defaults. When absent, the current settings are kept." + } + ] + } + }, + "required": [ + "html" + ], + "additionalProperties": false + }, "DocumentsV2BindQueryModelBody": { "type": "object", "properties": { @@ -32143,7 +32616,7 @@ "branch_id": { "type": "string", "format": "uuid", - "description": "Optional branch ID to run against. Must be a branch of the prompt set's model.", + "description": "Optional branch ID to run against. Must be a branch of the prompt set's model — for a shared extension without isolated branches, a branch of its parent shared model (requires Querier access on the parent).", "example": "440e8400-e29b-41d4-a716-446655440006" }, "repeat_count": { @@ -32557,7 +33030,7 @@ "accessBoost": { "type": "boolean", "default": false, - "description": "Whether to grant access boost. Rejected with 403 when the organization has AccessBoost turned off." + "description": "Whether to grant access boost. Rejected with 403 when the organization has AccessBoost turned off, or when a user-scoped key lacks boost provisioning." }, "role": { "type": "string", @@ -32612,7 +33085,7 @@ "properties": { "accessBoost": { "type": "boolean", - "description": "Whether to grant access boost. Rejected with 403 when the organization has AccessBoost turned off." + "description": "Whether to grant access boost. Rejected with 403 when the organization has AccessBoost turned off, or when a user-scoped key lacks boost provisioning." }, "role": { "type": "string", @@ -39007,7 +39480,7 @@ }, "/api/v1/ai/jobs": { "post": { - "description": "Submit a new AI job for asynchronous execution. The AI will analyze the prompt, generate and execute queries against the specified model, and produce a summarized answer. Jobs are processed by a background worker and typically complete within 15–60 seconds. Use GET /api/v1/ai/jobs/{jobId} to poll for status, or configure a webhookUrl to receive a notification when the job completes. Optionally continue an existing conversation by providing a conversationId. The effective user's per-connector AI toggles (set in the chat + menu) govern which integration tools the agent may use.", + "description": "Submit a new AI job for asynchronous execution. The AI will analyze the prompt, generate and execute queries against the specified model, and produce a summarized answer. Jobs are processed by a background worker and typically complete within 15–60 seconds. Use GET /api/v1/ai/jobs/{jobId} to poll for status, or configure a webhookUrl to receive a notification when the job completes. Optionally continue an existing conversation by providing a conversationId. The effective user's per-connector AI toggles (set in the chat + menu) govern which integration tools the agent may use. The agent can also build and modify dashboards. A dashboard it creates is published; an edit to an existing dashboard lands on that dashboard's draft, and the job publishes that draft only when the prompt asks it to (for example \"fix the revenue tile and publish it\"). Publishing makes the draft live for everyone who views the dashboard, and takes any changes already sitting in that draft live with it.", "operationId": "aiJobSubmit", "summary": "Submit an AI job", "tags": [ @@ -39262,6 +39735,113 @@ } } }, + "/api/v1/ai/jobs/{jobId}/feedback": { + "post": { + "description": "Rate an AI job's response — the same thumbs up / thumbs down (plus optional comment) a user can give under a response in the Omni chat UI or in Slack. The job must have finished (COMPLETE or FAILED); any other state returns 409. The completion webhook fires just before the stored state reaches COMPLETE, so poll GET /api/v1/ai/jobs/{jobId} and rate once it reports COMPLETE rather than rating from inside a webhook handler. Feedback is append-only: each call records a separate event, so submit once per job. Feedback is not returned by any read endpoint. User-scoped keys can only rate their own jobs; organization keys can rate any job in the organization and may pass `userId` to attribute the feedback to a specific user — use the same `userId` the job was submitted with.", + "operationId": "aiJobFeedbackSubmit", + "summary": "Rate an AI job's response", + "tags": [ + "AI" + ], + "parameters": [ + { + "schema": { + "type": "string", + "format": "uuid", + "description": "The unique identifier of the AI job", + "example": "123e4567-e89b-12d3-a456-426614174000" + }, + "required": true, + "description": "The unique identifier of the AI job", + "name": "jobId", + "in": "path" + }, + { + "schema": { + "type": "string", + "format": "uuid", + "description": "Target user membership ID (for org-scoped API keys)" + }, + "required": false, + "description": "Target user membership ID (for org-scoped API keys)", + "name": "userId", + "in": "query" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AiJobFeedbackBody" + } + } + } + }, + "responses": { + "200": { + "description": "Feedback recorded.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AiJobFeedbackResponse" + } + } + } + }, + "400": { + "description": "Invalid job ID (must be a UUID) or request body — unknown rating, unrecognized field, or a comment that is empty or over 5000 characters.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError400" + } + } + } + }, + "401": { + "description": "Missing or invalid API key.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError401" + } + } + } + }, + "403": { + "description": "Permission denied. AI query generation is disabled for the organization, the caller lacks AI access on the job's model, or a user-scoped key passed a different user's `userId`.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError403" + } + } + } + }, + "404": { + "description": "Job not found. The job may not exist, may belong to a different organization, or — for user-scoped keys — may belong to a different user. `userId` not found in the organization also returns 404.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError404" + } + } + } + }, + "409": { + "description": "The job is not COMPLETE or FAILED. For a QUEUED, EXECUTING, or DELIVERING job, poll GET /api/v1/ai/jobs/{jobId} and retry once it finishes; a CANCELLED job can never be rated.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError409" + } + } + } + } + } + } + }, "/api/v1/ai/jobs/{jobId}/result": { "get": { "description": "Retrieve the full result of a completed AI job, including all actions taken by the AI (queries generated, data retrieved) and the final summarized answer. Results are only available for jobs in COMPLETE state and are retained for 14 days after completion. The response is streamed directly from storage.", @@ -40156,7 +40736,7 @@ }, "/api/v1/ai/routines": { "get": { - "description": "List routines for the calling user, newest first. Includes routines paused by the owner or disabled by Omni, but excludes deleted routines. Use `pageInfo.nextCursor` from one response as the `cursor` query parameter on the next request. Organization API keys can pass `?userId=` to list routines for a specific organization member.", + "description": "List routine summaries for the calling user. A conditional routine includes its condition text and type; retrieve the individual routine to inspect its persisted semantic query. Results are newest first by default; pass `sortField=name` with `sortDirection=asc` or `desc` to sort by name. Includes routines paused by the owner or disabled by Omni, but excludes deleted routines. Use `pageInfo.nextCursor` from one response as the `cursor` query parameter on the next request. Organization API keys can pass `?userId=` to list routines for a specific organization member.", "operationId": "routinesList", "summary": "List routines", "tags": [ @@ -40238,7 +40818,7 @@ } }, "400": { - "description": "Invalid pagination cursor or `userId` value.", + "description": "Invalid pagination, sorting, `userId`, or unknown query parameter.", "content": { "application/json": { "schema": { @@ -40258,7 +40838,7 @@ } }, "403": { - "description": "AI routines or AI query generation are not enabled for the organization, or a user-scoped API key tried to list routines for another user.", + "description": "AI routines or AI query generation are not enabled for the organization, the caller lacks AI access, or a user-scoped API key tried to list routines for another user.", "content": { "application/json": { "schema": { @@ -40280,7 +40860,7 @@ } }, "post": { - "description": "Create a routine that runs a saved prompt on a schedule and delivers the AI response through a single destination — email (one or more recipients / user groups) or Slack (a single channel or direct message). Each scheduled run executes once using the routine owner's permissions, and every recipient receives the same result. Organization API keys can pass `?userId=` to create the routine for a specific organization member.", + "description": "Create a routine that runs a saved prompt on a schedule and delivers the AI response through a single destination — email (one or more recipients / user groups) or Slack (a single channel or direct message). Each scheduled run executes once using the routine owner's permissions, and every recipient receives the same result. Supply `condition` to deliver only when a stated condition holds. Omni returns the generated semantic query and, by default, runs it once before creation to verify it and establish the initial comparison baseline. Set `condition.verify` to false to skip that query run. Organization API keys can pass `?userId=` to create the routine for a specific organization member.", "operationId": "routineCreate", "summary": "Create a routine", "tags": [ @@ -40321,11 +40901,11 @@ } }, "400": { - "description": "Invalid request body, recipient configuration, schedule, or timezone. Also returned when the schedule is more frequent than the organization allows, and, with code `no_condition_detected`, for `gating: \"alert\"` (no delivery condition can be honored from the prompt alone).", + "description": "Invalid request body, recipient configuration, schedule, or timezone. Also returned when the destination type is disabled by the organization's delivery destination settings, when the schedule is more frequent than the organization allows, when Omni could not compose a working condition query, or when requested verification failed. Condition failures include a stable `code`: `not_expressible`, `no_query`, `malformed_query`, or `dry_run_failed`. Nothing is created on any of these.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/RoutineCreateApiError400" + "$ref": "#/components/schemas/RoutineConditionApiError400" } } } @@ -40340,104 +40920,18 @@ } } }, - "403": { - "description": "AI routines or AI query generation are not enabled for the organization, the API key cannot act on behalf of the requested user, the request used `gating: \"alert\"` but conditional routines are not enabled for the organization, or the target user is an embed user (embed users cannot own routines).", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ApiError403" - } - } - } - }, - "404": { - "description": "Model, branch, or topic not found, or not accessible to the requested user. Also returned when the `userId` membership has not accepted its invitation to the organization yet.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ApiError404" - } - } - } - }, - "429": { - "description": "The resolved user already has the maximum number of active routines.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ApiError429" - } - } - } - } - } - } - }, - "/api/v1/ai/routines/{id}": { - "get": { - "description": "Get a single routine, including the status of its most recent completed run.", - "operationId": "routineGet", - "summary": "Get a routine", - "tags": [ - "AI Routines" - ], - "parameters": [ - { - "schema": { - "type": "string", - "format": "uuid", - "description": "The UUID of the routine." - }, - "required": true, - "description": "The UUID of the routine.", - "name": "id", - "in": "path" - }, - { - "schema": { - "type": "string", - "format": "uuid", - "description": "Target user membership ID (for org-scoped API keys)" - }, - "required": false, - "description": "Target user membership ID (for org-scoped API keys)", - "name": "userId", - "in": "query" - } - ], - "responses": { - "200": { - "description": "Routine details.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RoutineResponse" - } - } - } - }, - "400": { - "description": "Invalid routine ID.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ApiError400" - } - } - } - }, - "401": { - "description": "Missing or invalid API key.", + "402": { + "description": "Composing or verifying the delivery condition needs AI, and the organization is over its AI credit limit. The body carries the stable reason code `shutoff`.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ApiError401" + "$ref": "#/components/schemas/AiCreditShutoffError" } } } }, "403": { - "description": "AI routines or AI query generation are not enabled for the organization, or a user-scoped API key tried to access another user's routine.", + "description": "AI routines or AI query generation are not enabled for the organization, the API key cannot act on behalf of the requested user, the request supplied a `condition` but conditional routines are not enabled for the organization, or the target user is an embed user (embed users cannot own routines).", "content": { "application/json": { "schema": { @@ -40447,7 +40941,7 @@ } }, "404": { - "description": "Routine not found or has been deleted.", + "description": "Model, branch, or topic not found, or not accessible to the requested user. Also returned when the `userId` membership has not accepted its invitation to the organization yet.", "content": { "application/json": { "schema": { @@ -40455,107 +40949,233 @@ } } } - } - } - }, - "patch": { - "description": "Update a routine. All request fields are optional, and only supplied fields are changed. Supplying `destination` replaces the full recipient configuration.", - "operationId": "routineUpdate", - "summary": "Update a routine", - "tags": [ - "AI Routines" - ], - "parameters": [ - { - "schema": { - "type": "string", - "format": "uuid", - "description": "The UUID of the routine." - }, - "required": true, - "description": "The UUID of the routine.", - "name": "id", - "in": "path" }, - { - "schema": { - "type": "string", - "format": "uuid", - "description": "Target user membership ID (for org-scoped API keys)" - }, - "required": false, - "description": "Target user membership ID (for org-scoped API keys)", - "name": "userId", - "in": "query" - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RoutineUpdateBody" - } - } - } - }, - "responses": { - "200": { - "description": "Updated routine details.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RoutineResponse" - } - } - } - }, - "400": { - "description": "Invalid routine ID, request body, recipient configuration, schedule, or timezone.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RoutineCreateApiError400" - } - } - } - }, - "401": { - "description": "Missing or invalid API key.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ApiError401" - } - } - } - }, - "403": { - "description": "AI routines or AI query generation are not enabled for the organization, or a user-scoped API key tried to update another user's routine.", + "429": { + "description": "The resolved user already has the maximum number of active routines.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ApiError403" + "$ref": "#/components/schemas/ApiError429" } } } }, - "404": { - "description": "Routine not found or has been deleted.", + "503": { + "description": "An AI or query service was temporarily unavailable while composing or verifying the delivery condition. Retryable.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ApiError404" + "$ref": "#/components/schemas/RoutineConditionApiError503" } } } } } - }, - "delete": { - "description": "Delete a routine. It stops running immediately and no longer appears in list or get responses.", - "operationId": "routineDelete", - "summary": "Delete a routine", + } + }, + "/api/v1/ai/routines/{id}": { + "get": { + "description": "Get a single routine, including its persisted condition query and the status of its most recent completed run. Verification results belong to the POST or PATCH operation that generated a condition and are not returned by GET.", + "operationId": "routineGet", + "summary": "Get a routine", + "tags": [ + "AI Routines" + ], + "parameters": [ + { + "schema": { + "type": "string", + "format": "uuid", + "description": "The UUID of the routine." + }, + "required": true, + "description": "The UUID of the routine.", + "name": "id", + "in": "path" + }, + { + "schema": { + "type": "string", + "format": "uuid", + "description": "Target user membership ID (for org-scoped API keys)" + }, + "required": false, + "description": "Target user membership ID (for org-scoped API keys)", + "name": "userId", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Routine details.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RoutineResponse" + } + } + } + }, + "400": { + "description": "Invalid routine ID.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError400" + } + } + } + }, + "401": { + "description": "Missing or invalid API key.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError401" + } + } + } + }, + "403": { + "description": "AI routines or AI query generation are not enabled for the organization, or a user-scoped API key tried to access another user's routine.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError403" + } + } + } + }, + "404": { + "description": "Routine not found or has been deleted.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError404" + } + } + } + } + } + }, + "patch": { + "description": "Update a routine. All request fields are optional, and only supplied fields are changed. Supplying `destination` replaces the full recipient configuration. Supplying a new `condition` recomposes it and verifies it by default; `null` removes it. Every conditional routine response includes its persisted semantic query; a newly derived condition also includes its verification result.", + "operationId": "routineUpdate", + "summary": "Update a routine", + "tags": [ + "AI Routines" + ], + "parameters": [ + { + "schema": { + "type": "string", + "format": "uuid", + "description": "The UUID of the routine." + }, + "required": true, + "description": "The UUID of the routine.", + "name": "id", + "in": "path" + }, + { + "schema": { + "type": "string", + "format": "uuid", + "description": "Target user membership ID (for org-scoped API keys)" + }, + "required": false, + "description": "Target user membership ID (for org-scoped API keys)", + "name": "userId", + "in": "query" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RoutineUpdateBody" + } + } + } + }, + "responses": { + "200": { + "description": "Updated routine details.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RoutineUpdateResponse" + } + } + } + }, + "400": { + "description": "Invalid routine ID, request body, recipient configuration, schedule, or timezone. Also returned when a supplied destination's type is disabled by the organization's delivery destination settings, when Omni could not compose a working replacement condition, or when requested verification failed. Condition failures include a stable `code`: `not_expressible`, `no_query`, `malformed_query`, or `dry_run_failed`. Nothing is updated on these failures.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RoutineConditionApiError400" + } + } + } + }, + "401": { + "description": "Missing or invalid API key.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError401" + } + } + } + }, + "402": { + "description": "Composing or verifying the replacement condition needs AI, and the organization is over its AI credit limit. The body carries the stable reason code `shutoff`.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AiCreditShutoffError" + } + } + } + }, + "403": { + "description": "AI routines, AI query generation, or conditional routines are not enabled for the organization; the routine owner lacks AI access to its model; or a user-scoped API key tried to update another user's routine.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError403" + } + } + } + }, + "404": { + "description": "Routine or routine-owner membership not found, or the routine has been deleted.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError404" + } + } + } + }, + "503": { + "description": "An AI or query service was temporarily unavailable while composing or verifying the replacement condition. Retryable.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RoutineConditionApiError503" + } + } + } + } + } + }, + "delete": { + "description": "Delete a routine. It stops running immediately and no longer appears in list or get responses.", + "operationId": "routineDelete", + "summary": "Delete a routine", "tags": [ "AI Routines" ], @@ -44754,6 +45374,17 @@ "description": "Free-text keywords matched against dashboard names, descriptions, query names, folder names, labels, and creator names.", "name": "q", "in": "query" + }, + { + "schema": { + "type": "string", + "format": "uuid", + "description": "Target user membership ID (for org-scoped API keys)" + }, + "required": false, + "description": "Target user membership ID (for org-scoped API keys)", + "name": "userId", + "in": "query" } ], "responses": { @@ -44778,7 +45409,7 @@ }, "/api/v1/dashboards/{identifier}/download": { "post": { - "description": "The artifact includes cross-model tiles the download runs as (the target user when one is named, else the API key’s user); tiles on a model they cannot query are omitted.", + "description": "The artifact includes cross-model tiles the download runs as (the target user when one is named, else the API key’s user); tiles on a model they cannot query are omitted, as are tiles with no layout entry (which the dashboard also does not display).", "operationId": "dashboardsDownload", "summary": "Initiate dashboard download", "tags": [ @@ -45266,7 +45897,7 @@ }, "/api/v1/documents/{identifier}": { "get": { - "description": "Retrieves a document's configuration in a format compatible with PUT for round-trip editing. GET a document, modify the response, and PUT it back to update. Only dashboard documents are supported; analysis documents return 400.", + "description": "Retrieves a document's full configuration, including its query presentations. Only dashboard documents are supported; analysis documents return 400. To edit a document, use the v2 `/draft` routes.", "operationId": "documentsGet", "summary": "Get document", "tags": [ @@ -45310,252 +45941,6 @@ } } }, - "put": { - "deprecated": true, - "description": "**Deprecated** — use `PATCH /api/v2/documents/{identifier}/draft` (and the related `/draft` routes). Removed on July 31, 2026: this endpoint now returns `410` unless your organization has been granted a migration extension. Contact support if you need one.\n\nUpdates a document with the specified identifier. This endpoint performs a full resource replacement — all required fields must be provided and existing query presentations are replaced entirely. Only dashboard documents are supported; analysis documents and documents without an associated dashboard return 400. For published documents, the update goes through a draft/publish workflow automatically; if a draft already exists, the request returns 409 unless `clearExistingDraft` is set to `true`.", - "operationId": "documentsPut", - "summary": "Replace document (full replacement)", - "tags": [ - "Documents" - ], - "parameters": [ - { - "schema": { - "type": "string", - "description": "Document identifier (either document ID or identifier slug)", - "example": "abc123" - }, - "required": true, - "description": "Document identifier (either document ID or identifier slug)", - "name": "identifier", - "in": "path" - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DocumentsPutBody" - } - } - } - }, - "responses": { - "200": { - "description": "Document replaced successfully", - "headers": { - "Deprecation": { - "schema": { - "type": "string", - "enum": [ - "true" - ], - "description": "Marks the endpoint as deprecated." - }, - "required": true, - "description": "Marks the endpoint as deprecated." - }, - "Link": { - "schema": { - "type": "string", - "description": "Points to the v2 successor resource.", - "example": "; rel=\"successor-version\"" - }, - "required": true, - "description": "Points to the v2 successor resource." - }, - "Sunset": { - "schema": { - "type": "string", - "description": "Date the endpoint will be removed, in RFC 1123 form (RFC 8594).", - "example": "Fri, 31 Jul 2026 00:00:00 GMT" - }, - "required": true, - "description": "Date the endpoint will be removed, in RFC 1123 form (RFC 8594)." - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DocumentsPutResponse" - } - } - } - }, - "400": { - "description": "Invalid request body, missing required fields, or validation error (also returned for analysis documents and documents without an associated dashboard)" - }, - "401": { - "description": "Authentication required" - }, - "403": { - "description": "Insufficient permissions to update the document" - }, - "404": { - "description": "Document not found" - }, - "409": { - "description": "Draft already exists - set clearExistingDraft to true to discard it and proceed" - }, - "410": { - "description": "Endpoint has been removed; the organization does not hold a migration extension", - "headers": { - "Deprecation": { - "schema": { - "type": "string", - "enum": [ - "true" - ], - "description": "Marks the endpoint as deprecated." - }, - "required": true, - "description": "Marks the endpoint as deprecated." - }, - "Link": { - "schema": { - "type": "string", - "description": "Points to the v2 successor resource.", - "example": "; rel=\"successor-version\"" - }, - "required": true, - "description": "Points to the v2 successor resource." - }, - "Sunset": { - "schema": { - "type": "string", - "description": "Date the endpoint will be removed, in RFC 1123 form (RFC 8594).", - "example": "Fri, 31 Jul 2026 00:00:00 GMT" - }, - "required": true, - "description": "Date the endpoint will be removed, in RFC 1123 form (RFC 8594)." - } - } - } - } - }, - "patch": { - "deprecated": true, - "description": "**Deprecated** — use `PATCH /api/v2/documents/{identifier}/draft` (and the related `/draft` routes). Removed on July 31, 2026: this endpoint now returns `410` unless your organization has been granted a migration extension. Contact support if you need one.\n\nUpdates a document's name, description, and/or identifier. This is a partial update — only provided fields are modified, and at least one of `name`, `description`, or `identifier` must be supplied. When `identifier` is changed, the previous identifier is retained in the document identifier history and continues to redirect. For published documents, the update goes through a draft/publish workflow automatically.", - "operationId": "documentsUpdate", - "summary": "Rename document", - "tags": [ - "Documents" - ], - "parameters": [ - { - "schema": { - "type": "string", - "description": "Document identifier (either document ID or identifier slug)", - "example": "abc123" - }, - "required": true, - "description": "Document identifier (either document ID or identifier slug)", - "name": "identifier", - "in": "path" - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DocumentsUpdateBody" - } - } - } - }, - "responses": { - "200": { - "description": "Document updated successfully", - "headers": { - "Deprecation": { - "schema": { - "type": "string", - "enum": [ - "true" - ], - "description": "Marks the endpoint as deprecated." - }, - "required": true, - "description": "Marks the endpoint as deprecated." - }, - "Link": { - "schema": { - "type": "string", - "description": "Points to the v2 successor resource.", - "example": "; rel=\"successor-version\"" - }, - "required": true, - "description": "Points to the v2 successor resource." - }, - "Sunset": { - "schema": { - "type": "string", - "description": "Date the endpoint will be removed, in RFC 1123 form (RFC 8594).", - "example": "Fri, 31 Jul 2026 00:00:00 GMT" - }, - "required": true, - "description": "Date the endpoint will be removed, in RFC 1123 form (RFC 8594)." - } - }, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DocumentsUpdateResponse" - } - } - } - }, - "400": { - "description": "Invalid request body or validation error (e.g. missing name/description/identifier, name too long, identifier already in use)" - }, - "401": { - "description": "Authentication required" - }, - "403": { - "description": "Permission denied - EDITOR role required" - }, - "404": { - "description": "Document not found" - }, - "409": { - "description": "Draft already exists - set clearExistingDraft to true to discard it and proceed" - }, - "410": { - "description": "Endpoint has been removed; the organization does not hold a migration extension", - "headers": { - "Deprecation": { - "schema": { - "type": "string", - "enum": [ - "true" - ], - "description": "Marks the endpoint as deprecated." - }, - "required": true, - "description": "Marks the endpoint as deprecated." - }, - "Link": { - "schema": { - "type": "string", - "description": "Points to the v2 successor resource.", - "example": "; rel=\"successor-version\"" - }, - "required": true, - "description": "Points to the v2 successor resource." - }, - "Sunset": { - "schema": { - "type": "string", - "description": "Date the endpoint will be removed, in RFC 1123 form (RFC 8594).", - "example": "Fri, 31 Jul 2026 00:00:00 GMT" - }, - "required": true, - "description": "Date the endpoint will be removed, in RFC 1123 form (RFC 8594)." - } - } - } - } - }, "delete": { "operationId": "documentsDelete", "summary": "Delete document", @@ -45620,7 +46005,7 @@ ], "responses": { "200": { - "description": "Every query in the document — the dashboard's query presentation collection, which mirrors the workbook's tabs. The dashboard layout decides which of these render, so a query here may not appear on the dashboard.", + "description": "Every saved query in the document, in tab order — for a dashboard, app, report, or plain workbook alike. Where the document has a presentation, that layout decides which of these render, so a query here may not appear on it.", "content": { "application/json": { "schema": { @@ -45636,7 +46021,7 @@ "description": "Permission denied" }, "404": { - "description": "Document not found, or the document has no dashboard (required today; see the endpoint README)" + "description": "Document not found, or the identifier names an unsaved analysis" } } } @@ -46876,11 +47261,567 @@ ], "responses": { "200": { - "description": "Document state. A workbook-only document (no dashboard layout yet) returns only the workbook-scoped fields (`name`, `description`, `queryPresentations`); the dashboard-scoped `containers`, `controls`, and `settings` are omitted until a layout exists.", + "description": "Document state. A workbook-only document (no dashboard layout yet) returns only the workbook-scoped fields (`name`, `description`, `queryPresentations`); the dashboard-scoped `containers`, `controls`, and `settings` are omitted until a layout exists. An app document returns the workbook-scoped fields plus an `app` slice whose `url` is the app sub-resource, where the app’s HTML and settings are read and written; the slice is withheld when the organization has apps disabled.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DocumentsV2ReadResponse" + } + } + } + }, + "401": { + "description": "Authentication required." + }, + "403": { + "description": "Insufficient permissions to read the document." + }, + "404": { + "description": "Document not found." + }, + "422": { + "description": "The document uses the classic dashboard layout; upgrade to the advanced layout first." + } + } + } + }, + "/api/v2/documents/{identifier}/draft": { + "patch": { + "description": "Create a new draft on the published document and apply the patch. No auto-publish — the response includes the new `draftIdentifier` for follow-up calls.\n\nPass an optional `branchId` to attach the draft to a branch; omit it for a draft on the main (unpublished) workspace.", + "operationId": "documentsV2PatchDraft", + "summary": "Create draft and patch document", + "tags": [ + "Documents" + ], + "parameters": [ + { + "schema": { + "type": "string", + "description": "Document identifier — either the URL slug (e.g. `abc123`) or the canonical workbook UUID.", + "example": "abc123" + }, + "required": true, + "description": "Document identifier — either the URL slug (e.g. `abc123`) or the canonical workbook UUID.", + "name": "identifier", + "in": "path" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DocumentsV2CreateDraftBody" + } + } + } + }, + "responses": { + "200": { + "description": "Draft created and patch applied successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DocumentsV2PatchDraftResponse" + } + } + } + }, + "400": { + "description": "Invalid request body or schema validation error (e.g. unknown top-level field, name too long, query presentation cap exceeded, a `modelId` that differs from the document’s immutable base model, or a `workbookModelId` that differs from the read-only value a GET returns)." + }, + "401": { + "description": "Authentication required." + }, + "403": { + "description": "Insufficient permissions to update the document, or the target is an app document and the organization has apps disabled." + }, + "404": { + "description": "Document or branch not found." + }, + "405": { + "description": "Method not allowed." + }, + "409": { + "description": "The target is not a published document (drafts only attach to published documents), a concurrent request just created the layout for this document (retry), or a supplied `app` does not round-trip: its `url` does not address this document’s app, or the document has no app (`app` is read-only — omit it, or send it as a GET returned it)." + }, + "422": { + "description": "The document cannot satisfy the patch: a classic-layout dashboard (upgrade to the advanced layout first), a workbook-only document patched with dashboard-scoped fields but no `containers` payload (or an empty one), or an app document patched with dashboard-scoped fields (a document carries at most one of a dashboard or an app — never both)." + } + } + } + }, + "/api/v2/documents/{identifier}/draft/{draftIdentifier}": { + "get": { + "description": "Read the named draft's state. Returns the full `DocumentsV2ReadResponse` shape — same as the live-state read endpoint.\n\nThe response is structured so a caller can take it verbatim and submit it as the body of the draft PATCH routes. Tiles in `queryPresentations.data` are keyed by a stable record key (e.g. `\"1\"`, `\"2\"`) — the server uses that key to identify existing tiles for updates, so callers do not need to track or send any other identifier. Control IDs and container `instanceKey` / `referenceKey` values also round-trip unchanged.", + "operationId": "documentsV2GetDraft", + "summary": "Read draft state", + "tags": [ + "Documents" + ], + "parameters": [ + { + "schema": { + "type": "string", + "description": "Draft workbook identifier (see `PATCH /api/v2/documents/{identifier}/draft`).", + "example": "def456" + }, + "required": true, + "description": "Draft workbook identifier (see `PATCH /api/v2/documents/{identifier}/draft`).", + "name": "draftIdentifier", + "in": "path" + }, + { + "schema": { + "type": "string", + "description": "Published document identifier.", + "example": "abc123" + }, + "required": true, + "description": "Published document identifier.", + "name": "identifier", + "in": "path" + }, + { + "schema": { + "type": "string", + "enum": [ + "0", + "1", + "true", + "false" + ], + "description": "Set `true` or `1` to pretty-print (2-space indent) the response; `false` / `0` (the default) is compact. Key ordering is deterministic regardless." + }, + "required": false, + "description": "Set `true` or `1` to pretty-print (2-space indent) the response; `false` / `0` (the default) is compact. Key ordering is deterministic regardless.", + "name": "pretty", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Draft state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DocumentsV2ReadResponse" + } + } + } + }, + "401": { + "description": "Authentication required." + }, + "403": { + "description": "Insufficient permissions to read the draft." + }, + "404": { + "description": "Document or draft not found." + }, + "422": { + "description": "The draft uses the classic dashboard layout; upgrade to the advanced layout first." + } + } + }, + "patch": { + "description": "Apply the patch to an existing draft addressed by `draftIdentifier`. Pure apply — no draft creation, no publish.", + "operationId": "documentsV2PatchDraftByIdentifier", + "summary": "Patch draft", + "tags": [ + "Documents" + ], + "parameters": [ + { + "schema": { + "type": "string", + "description": "Draft workbook identifier (see `PATCH /api/v2/documents/{identifier}/draft`).", + "example": "def456" + }, + "required": true, + "description": "Draft workbook identifier (see `PATCH /api/v2/documents/{identifier}/draft`).", + "name": "draftIdentifier", + "in": "path" + }, + { + "schema": { + "type": "string", + "description": "Published document identifier.", + "example": "abc123" + }, + "required": true, + "description": "Published document identifier.", + "name": "identifier", + "in": "path" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DocumentsV2PatchDraftBody" + } + } + } + }, + "responses": { + "200": { + "description": "Patch applied to draft successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DocumentsV2PatchDraftResponse" + } + } + } + }, + "400": { + "description": "Invalid request body or schema validation error (e.g. unknown top-level field, name too long, query presentation cap exceeded, a `modelId` that differs from the document’s immutable base model, or a `workbookModelId` that differs from the read-only value a GET returns)." + }, + "401": { + "description": "Authentication required." + }, + "403": { + "description": "Insufficient permissions to update the draft, or the target is an app draft and the organization has apps disabled." + }, + "404": { + "description": "Document or draft not found." + }, + "405": { + "description": "Method not allowed." + }, + "409": { + "description": "The target is not a published document (drafts only attach to published documents), a concurrent request just created the layout for this document (retry), or a supplied `app` does not round-trip: its `url` does not address this document’s app, or the document has no app (`app` is read-only — omit it, or send it as a GET returned it)." + }, + "422": { + "description": "The draft cannot satisfy the patch: a classic-layout dashboard (upgrade to the advanced layout first), a workbook-only draft patched with dashboard-scoped fields but no `containers` payload (or an empty one), or an app draft patched with dashboard-scoped fields (a document carries at most one of a dashboard or an app — never both)." + } + } + } + }, + "/api/v2/documents/{identifier}/draft/{draftIdentifier}/dashboard": { + "delete": { + "description": "Remove the dashboard from an existing draft, leaving a workbook-only document. Its schedules are removed too, but at publish time (see below), not on this call. Parity with the UI’s \"Remove dashboard\" action, and the inverse of adding a dashboard via a `containers` patch.\n\nOperates only on the draft named by `draftIdentifier`, which the caller creates first via `PATCH …/draft`. Requiring an explicit draft keeps the removal from silently reusing (and clobbering) a draft that holds other unpublished work.\n\nNo auto-publish — publish via `POST …/draft/publish` to make the document workbook-only (publishing also clears the previously-published dashboard’s schedules). Idempotent: a draft that is already workbook-only returns 200 unchanged.", + "operationId": "documentsV2RemoveDashboard", + "summary": "Remove dashboard from document", + "tags": [ + "Documents" + ], + "parameters": [ + { + "schema": { + "type": "string", + "description": "Draft workbook identifier (see `PATCH /api/v2/documents/{identifier}/draft`).", + "example": "def456" + }, + "required": true, + "description": "Draft workbook identifier (see `PATCH /api/v2/documents/{identifier}/draft`).", + "name": "draftIdentifier", + "in": "path" + }, + { + "schema": { + "type": "string", + "description": "Published document identifier.", + "example": "abc123" + }, + "required": true, + "description": "Published document identifier.", + "name": "identifier", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Dashboard removed on the draft (or a no-op when already workbook-only); the response carries the `draftIdentifier`.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DocumentsV2PatchDraftResponse" + } + } + } + }, + "401": { + "description": "Authentication required." + }, + "403": { + "description": "Insufficient permissions to update the document." + }, + "404": { + "description": "Document or draft not found." + }, + "405": { + "description": "Method not allowed." + }, + "409": { + "description": "The target is not a published document." + }, + "422": { + "description": "The document is an app, not a dashboard." + } + } + } + }, + "/api/v2/documents/{identifier}/app": { + "get": { + "description": "**Alpha.** The app sub-resource may change shape without a deprecation cycle while apps mature. The document routes are stable.\n\nRead the published app content: the full HTML plus `settings`. This is the only endpoint that returns the HTML body; the document GET carries only the `app` slice. Writes cap the HTML at 2 MiB — apps saved before the cap may read back larger. This reads the published app; an existing draft may have diverged, so read the draft state before building a draft write from this response.\n\nA document carries at most one of a dashboard or an app — never both; workbook-only is valid. The app HTML and settings live only at the app sub-resource routes; the document read carries an `app` slice pointing here, and the whole-document PATCH accepts that slice back only as it was read.", + "operationId": "documentsV2GetApp", + "summary": "Read app content", + "tags": [ + "Documents" + ], + "x-experimental": true, + "parameters": [ + { + "schema": { + "type": "string", + "description": "Document identifier — either the URL slug (e.g. `abc123`) or the canonical workbook UUID.", + "example": "abc123" + }, + "required": true, + "description": "Document identifier — either the URL slug (e.g. `abc123`) or the canonical workbook UUID.", + "name": "identifier", + "in": "path" + }, + { + "schema": { + "type": "string", + "enum": [ + "0", + "1", + "true", + "false" + ], + "description": "Set `true` or `1` to pretty-print (2-space indent) the response; `false` / `0` (the default) is compact. Key ordering is deterministic regardless." + }, + "required": false, + "description": "Set `true` or `1` to pretty-print (2-space indent) the response; `false` / `0` (the default) is compact. Key ordering is deterministic regardless.", + "name": "pretty", + "in": "query" + } + ], + "responses": { + "200": { + "description": "The published app content.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DocumentsV2AppContentResponse" + } + } + } + }, + "401": { + "description": "Authentication required." + }, + "403": { + "description": "Insufficient permissions to read the document." + }, + "404": { + "description": "Document not found, the document has no app, or the organization has apps disabled." + }, + "405": { + "description": "Method not allowed." + } + } + } + }, + "/api/v2/documents/{identifier}/draft/{draftIdentifier}/app": { + "get": { + "description": "**Alpha.** The app sub-resource may change shape without a deprecation cycle while apps mature. The document routes are stable.\n\nRead the app content on a named draft — the draft-side counterpart of `GET …/app`, and the read half of the draft write loop: app writes are last-write-wins whole-document replaces, so fetch the draft’s latest HTML here before building the next `PUT …/draft/{draftIdentifier}/app` body. Same response shape as the published read. Writes cap the HTML at 2 MiB — apps saved before the cap may read back larger, and a body over the cap is rejected on the way back in. The published document’s content is unaffected by draft edits — read it via `GET …/app`.\n\nA draft with no app is a 404 — including a draft that carries a dashboard, which can never carry an app.\n\nA document carries at most one of a dashboard or an app — never both; workbook-only is valid. The app HTML and settings live only at the app sub-resource routes; the document read carries an `app` slice pointing here, and the whole-document PATCH accepts that slice back only as it was read.", + "operationId": "documentsV2GetDraftApp", + "summary": "Read app content on a draft", + "tags": [ + "Documents" + ], + "x-experimental": true, + "parameters": [ + { + "schema": { + "type": "string", + "description": "Draft workbook identifier (see `PATCH /api/v2/documents/{identifier}/draft`).", + "example": "def456" + }, + "required": true, + "description": "Draft workbook identifier (see `PATCH /api/v2/documents/{identifier}/draft`).", + "name": "draftIdentifier", + "in": "path" + }, + { + "schema": { + "type": "string", + "description": "Published document identifier.", + "example": "abc123" + }, + "required": true, + "description": "Published document identifier.", + "name": "identifier", + "in": "path" + }, + { + "schema": { + "type": "string", + "enum": [ + "0", + "1", + "true", + "false" + ], + "description": "Set `true` or `1` to pretty-print (2-space indent) the response; `false` / `0` (the default) is compact. Key ordering is deterministic regardless." + }, + "required": false, + "description": "Set `true` or `1` to pretty-print (2-space indent) the response; `false` / `0` (the default) is compact. Key ordering is deterministic regardless.", + "name": "pretty", + "in": "query" + } + ], + "responses": { + "200": { + "description": "The draft’s app content.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DocumentsV2AppContentResponse" + } + } + } + }, + "401": { + "description": "Authentication required." + }, + "403": { + "description": "Insufficient permissions to read the document." + }, + "404": { + "description": "Document or draft not found, the draft has no app (a dashboard draft never does), or the organization has apps disabled." + }, + "405": { + "description": "Method not allowed." + } + } + }, + "put": { + "description": "**Alpha.** The app sub-resource may change shape without a deprecation cycle while apps mature. The document routes are stable.\n\nReplace the app HTML on an existing draft — creating the app when the draft is workbook-only — optionally replacing the app `settings` in the same call. Operates only on the draft named by `draftIdentifier`, which the caller creates first via `PATCH …/draft`; a published app is never addressable for writing, so every edit is draft-then-publish by construction. No auto-publish — publish via `POST …/draft/publish`.\n\nLast-write-wins, like every app write in the UI: the body is applied as given, with no expected-version precondition. Every write appends an immutable `app_history` revision, so nothing is lost — only the head moves.\n\nWrites never reject on host policy. External `