From d925a33a27f87663f87a8f29881809a6dac51278 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 28 Sep 2026 15:18:49 +0000 Subject: [PATCH 1/8] docs(api): aggregate asset health scores endpoint + path filter (APP-1713) Co-Authored-By: mika@elementary-data.com --- docs/api-reference/beta/openapi.json | 398 ++++++++++++++++++ docs/api/introduction.mdx | 43 ++ .../aggregate-asset-health-scores.mdx | 3 + docs/docs.json | 3 +- 4 files changed, 446 insertions(+), 1 deletion(-) create mode 100644 docs/api/reference/asset-health-scores/aggregate-asset-health-scores.mdx diff --git a/docs/api-reference/beta/openapi.json b/docs/api-reference/beta/openapi.json index 8e489f9a5..14605f5a0 100644 --- a/docs/api-reference/beta/openapi.json +++ b/docs/api-reference/beta/openapi.json @@ -1,6 +1,55 @@ { "components": { "schemas": { + "AggregatedHealthScore": { + "properties": { + "dimensions": { + "description": "Latest aggregated score per dimension. Dimensions with no results in the last 7 days are omitted.", + "items": { + "$ref": "#/components/schemas/DimensionHealthScore" + }, + "title": "Dimensions", + "type": "array" + }, + "total": { + "$ref": "#/components/schemas/AggregatedHealthScoreBucket", + "description": "Weighted score across all dimensions for the whole matching asset set, using the environment's configured dimension weights. Aggregated across the assets' test results, not an average of per-asset scores." + } + }, + "required": ["total", "dimensions"], + "title": "AggregatedHealthScore", + "type": "object" + }, + "AggregatedHealthScoreBucket": { + "properties": { + "bucket_at": { + "anyOf": [ + { + "format": "date-time", + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Start of the hourly bucket this score was computed for. `null` when no matching asset has test results in the window.", + "title": "Bucket At" + }, + "score": { + "description": "Health score in the range 0-1 for the most recent hourly bucket. `0` when no matching asset has test results in the window.", + "title": "Score", + "type": "number" + }, + "test_count": { + "description": "Number of test results that contributed to this score.", + "title": "Test Count", + "type": "integer" + } + }, + "required": ["score", "test_count", "bucket_at"], + "title": "AggregatedHealthScoreBucket", + "type": "object" + }, "ApiError": { "properties": { "code": { @@ -1726,6 +1775,289 @@ "tags": ["Asset health scores"] } }, + "/public/beta/{env_id}/asset-health-scores/aggregate": { + "get": { + "description": "Returns one health score card for the set of table assets matching the filters \u2014 the same number the Data Health dashboard shows for a filter or folder. Filters match `GET /assets/tables` plus `path` and intersect. The score is aggregated across the matching assets' test results in the last 7 days, not averaged from per-asset scores. When no matching asset has test results in the window, returns a zero card (`score` 0, `test_count` 0, `bucket_at` null, no dimensions).", + "operationId": "aggregate_asset_health_scores_public_beta__env_id__asset_health_scores_aggregate_get", + "parameters": [ + { + "description": "Environment identifier that scopes the request.", + "in": "path", + "name": "env_id", + "required": true, + "schema": { + "description": "Environment identifier that scopes the request.", + "title": "Env Id", + "type": "string" + } + }, + { + "description": "Aggregate only table assets with these ids.", + "in": "query", + "name": "ids", + "required": false, + "schema": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "maxItems": 1000, + "type": "array" + }, + { + "type": "null" + } + ], + "description": "Aggregate only table assets with these ids.", + "title": "Ids" + } + }, + { + "description": "Aggregate only table assets from these source types (e.g. `dbt`, `fivetran`).", + "in": "query", + "name": "source_types", + "required": false, + "schema": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "maxItems": 1000, + "type": "array" + }, + { + "type": "null" + } + ], + "description": "Aggregate only table assets from these source types (e.g. `dbt`, `fivetran`).", + "title": "Source Types" + } + }, + { + "description": "Aggregate only table assets tagged with any of these tags.", + "in": "query", + "name": "tags", + "required": false, + "schema": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "maxItems": 1000, + "type": "array" + }, + { + "type": "null" + } + ], + "description": "Aggregate only table assets tagged with any of these tags.", + "title": "Tags" + } + }, + { + "description": "Aggregate only table assets in these databases.", + "in": "query", + "name": "db_names", + "required": false, + "schema": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "maxItems": 1000, + "type": "array" + }, + { + "type": "null" + } + ], + "description": "Aggregate only table assets in these databases.", + "title": "Db Names" + } + }, + { + "description": "Aggregate only table assets in these schemas.", + "in": "query", + "name": "schema_names", + "required": false, + "schema": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "maxItems": 1000, + "type": "array" + }, + { + "type": "null" + } + ], + "description": "Aggregate only table assets in these schemas.", + "title": "Schema Names" + } + }, + { + "description": "Aggregate only table assets with these materializations (e.g. `table`, `view`, `incremental`).", + "in": "query", + "name": "materializations", + "required": false, + "schema": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "maxItems": 1000, + "type": "array" + }, + { + "type": "null" + } + ], + "description": "Aggregate only table assets with these materializations (e.g. `table`, `view`, `incremental`).", + "title": "Materializations" + } + }, + { + "description": "Return only table assets whose `path` equals one of these paths or is nested under one of them (`/`-separated, e.g. `models/marts` matches `models/marts/orders.sql`). Repeatable; assets without a `path` never match.", + "in": "query", + "name": "path", + "required": false, + "schema": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "maxItems": 1000, + "type": "array" + }, + { + "type": "null" + } + ], + "description": "Return only table assets whose `path` equals one of these paths or is nested under one of them (`/`-separated, e.g. `models/marts` matches `models/marts/orders.sql`). Repeatable; assets without a `path` never match.", + "title": "Path" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AggregatedHealthScore" + } + } + }, + "description": "Successful Response" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + }, + "description": "Invalid cursor or request parameters." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + }, + "description": "Authentication is required." + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + }, + "description": "The token does not have permission to access the resource." + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + }, + "description": "The requested resource was not found." + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + }, + "description": "Request validation failed." + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + }, + "description": "Rate limit exceeded.", + "headers": { + "RateLimit-Limit": { + "description": "Maximum requests allowed in the current window.", + "schema": { + "type": "integer" + } + }, + "RateLimit-Remaining": { + "description": "Requests remaining in the current window.", + "schema": { + "type": "integer" + } + }, + "RateLimit-Reset": { + "description": "Seconds until the current rate-limit window resets.", + "schema": { + "type": "integer" + } + }, + "Retry-After": { + "description": "Seconds to wait before retrying.", + "schema": { + "type": "integer" + } + } + } + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + }, + "description": "An unexpected server error occurred." + } + }, + "summary": "Aggregate asset health scores", + "tags": ["Asset health scores"] + } + }, "/public/beta/{env_id}/asset-health-scores/{asset_id}": { "get": { "description": "Fetches the current health score of a single asset. Returns 404 if the asset doesn't exist in this environment or has no test results in the last 7 days.", @@ -2131,6 +2463,28 @@ "title": "Tags" } }, + { + "description": "Return only assets whose `path` equals one of these paths or is nested under one of them (`/`-separated, e.g. `models/marts` matches `models/marts/orders.sql`). Repeatable; assets without a `path` never match.", + "in": "query", + "name": "path", + "required": false, + "schema": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "maxItems": 1000, + "type": "array" + }, + { + "type": "null" + } + ], + "description": "Return only assets whose `path` equals one of these paths or is nested under one of them (`/`-separated, e.g. `models/marts` matches `models/marts/orders.sql`). Repeatable; assets without a `path` never match.", + "title": "Path" + } + }, { "description": "Upserts feed: assets whose `synced_at` >= this time (inclusive). Orders by `synced_at`. Clamped to the last 90 days \u2014 an older time is treated as 90 days ago.", "in": "query", @@ -2440,6 +2794,28 @@ "title": "Bi Platforms" } }, + { + "description": "Return only BI assets whose `path` equals one of these paths or is nested under one of them (`/`-separated, e.g. `models/marts` matches `models/marts/orders.sql`). Repeatable; assets without a `path` never match.", + "in": "query", + "name": "path", + "required": false, + "schema": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "maxItems": 1000, + "type": "array" + }, + { + "type": "null" + } + ], + "description": "Return only BI assets whose `path` equals one of these paths or is nested under one of them (`/`-separated, e.g. `models/marts` matches `models/marts/orders.sql`). Repeatable; assets without a `path` never match.", + "title": "Path" + } + }, { "description": "Upserts feed: assets whose `synced_at` >= this time (inclusive). Orders by `synced_at`. Clamped to the last 90 days \u2014 an older time is treated as 90 days ago.", "in": "query", @@ -2933,6 +3309,28 @@ "title": "Materializations" } }, + { + "description": "Return only table assets whose `path` equals one of these paths or is nested under one of them (`/`-separated, e.g. `models/marts` matches `models/marts/orders.sql`). Repeatable; assets without a `path` never match.", + "in": "query", + "name": "path", + "required": false, + "schema": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "maxItems": 1000, + "type": "array" + }, + { + "type": "null" + } + ], + "description": "Return only table assets whose `path` equals one of these paths or is nested under one of them (`/`-separated, e.g. `models/marts` matches `models/marts/orders.sql`). Repeatable; assets without a `path` never match.", + "title": "Path" + } + }, { "description": "Upserts feed: assets whose `synced_at` >= this time (inclusive). Orders by `synced_at`. Clamped to the last 90 days \u2014 an older time is treated as 90 days ago.", "in": "query", diff --git a/docs/api/introduction.mdx b/docs/api/introduction.mdx index 00c145a27..1a50acfc8 100644 --- a/docs/api/introduction.mdx +++ b/docs/api/introduction.mdx @@ -66,6 +66,7 @@ Follow `next_cursor` until `has_more` is `false` — see [Pagination](/api/pagin | Latest test executions | `GET /{env_id}/latest-test-executions` | Current run per sub-test | | Test execution history | `GET /{env_id}/tests/{test_id}/executions` | History for one test | | Asset health scores | `GET /{env_id}/asset-health-scores` | Current health score per asset | +| Aggregate health score | `GET /{env_id}/asset-health-scores/aggregate` | One health score for a filtered set of tables | Every list endpoint supports a full scan and keyset pagination. Assets, columns, and tests also expose incremental feeds — see @@ -150,6 +151,48 @@ Each item has a `total` score and one entry per quality `dimension` [assets](/api/reference/assets/list-assets) on `asset_id`. - `dimension` is an extensible enum (see below). +### Aggregate health score for a set of assets + +**[`GET /asset-health-scores/aggregate`](/api/reference/asset-health-scores/aggregate-asset-health-scores)** +returns a single health score for a group of table assets — for example a dbt +folder, a schema, or everything with a tag — the same number the Elementary +data health dashboard shows when you filter it to that group. It is computed +over the matching assets' test results together, **not** as an average of +their per-asset scores. + +```bash +curl -G "https://prod.api.elementary-data.com/public/beta/$ENV_ID/asset-health-scores/aggregate" \ + -H "Authorization: Bearer $ELEMENTARY_TOKEN" \ + --data-urlencode "path=models/marts" \ + --data-urlencode "tags=finance" +``` + +```json +{ + "total": { "score": 0.91, "test_count": 12, "bucket_at": "2026-09-14T08:00:00" }, + "dimensions": [ + { "dimension": "freshness", "score": 0.99, "test_count": 3, "bucket_at": "2026-09-14T08:00:00" } + ] +} +``` + +- Only [table assets](/api/reference/assets/list-table-assets) are included. + Filter them with the same parameters as `GET /assets/tables`: `ids`, + `source_types`, `tags`, `db_names`, `schema_names`, `materializations`, and + `path`. Values within one parameter are OR-ed; different parameters are + AND-ed. With no filters, the score covers every monitored table you can see. +- `path` selects a folder: it matches assets whose `path` equals the value or is + nested under it (`models/marts` matches `models/marts/orders.sql` but not + `models/marts_legacy/orders.sql`). Repeat it to combine folders. The same + `path` filter is also available on `GET /assets`, `/assets/tables`, and + `/assets/bi`. +- Same scoring as above: scores are in the range `0`–`1`, computed from test + results in a fixed **last 7 days** window, and dimensions with no results are + omitted. +- If no matching asset has test results in the window, the response is still + `200` with a zero score: + `{"total": {"score": 0, "test_count": 0, "bucket_at": null}, "dimensions": []}`. + ## Forward compatibility Some string fields are **extensible enums**: they carry a value from a small, diff --git a/docs/api/reference/asset-health-scores/aggregate-asset-health-scores.mdx b/docs/api/reference/asset-health-scores/aggregate-asset-health-scores.mdx new file mode 100644 index 000000000..9931d2cde --- /dev/null +++ b/docs/api/reference/asset-health-scores/aggregate-asset-health-scores.mdx @@ -0,0 +1,3 @@ +--- +openapi: api-reference/beta/openapi.json get /public/beta/{env_id}/asset-health-scores/aggregate +--- diff --git a/docs/docs.json b/docs/docs.json index f48208af3..ab57fdda4 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -583,7 +583,8 @@ "group": "Asset health scores", "pages": [ "api/reference/asset-health-scores/list-asset-health-scores", - "api/reference/asset-health-scores/get-an-asset-health-score" + "api/reference/asset-health-scores/get-an-asset-health-score", + "api/reference/asset-health-scores/aggregate-asset-health-scores" ] } ] From 543339953f743b95811a38ef80114aaf35bec804 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 28 Sep 2026 15:22:46 +0000 Subject: [PATCH 2/8] docs(api): note trailing slash and no-filter aggregate semantics (APP-1713) Co-Authored-By: mika@elementary-data.com --- docs/api-reference/beta/openapi.json | 18 +++++++++--------- docs/api/introduction.mdx | 9 +++++---- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/docs/api-reference/beta/openapi.json b/docs/api-reference/beta/openapi.json index 14605f5a0..f19ce2304 100644 --- a/docs/api-reference/beta/openapi.json +++ b/docs/api-reference/beta/openapi.json @@ -1777,7 +1777,7 @@ }, "/public/beta/{env_id}/asset-health-scores/aggregate": { "get": { - "description": "Returns one health score card for the set of table assets matching the filters \u2014 the same number the Data Health dashboard shows for a filter or folder. Filters match `GET /assets/tables` plus `path` and intersect. The score is aggregated across the matching assets' test results in the last 7 days, not averaged from per-asset scores. When no matching asset has test results in the window, returns a zero card (`score` 0, `test_count` 0, `bucket_at` null, no dimensions).", + "description": "Returns one health score card for the set of table assets matching the filters \u2014 the same number the Data Health dashboard shows for a filter or folder. Filters match `GET /assets/tables` plus `path` and intersect; with no filters, every scored asset you can view is included. The score is aggregated across the matching assets' test results in the last 7 days, not averaged from per-asset scores. When no matching asset has test results in the window, returns a zero card (`score` 0, `test_count` 0, `bucket_at` null, no dimensions).", "operationId": "aggregate_asset_health_scores_public_beta__env_id__asset_health_scores_aggregate_get", "parameters": [ { @@ -1924,7 +1924,7 @@ } }, { - "description": "Return only table assets whose `path` equals one of these paths or is nested under one of them (`/`-separated, e.g. `models/marts` matches `models/marts/orders.sql`). Repeatable; assets without a `path` never match.", + "description": "Return only table assets whose `path` equals one of these paths or is nested under one of them (`/`-separated, e.g. `models/marts` matches `models/marts/orders.sql`; a trailing `/` is ignored). Repeatable; assets without a `path` never match.", "in": "query", "name": "path", "required": false, @@ -1941,7 +1941,7 @@ "type": "null" } ], - "description": "Return only table assets whose `path` equals one of these paths or is nested under one of them (`/`-separated, e.g. `models/marts` matches `models/marts/orders.sql`). Repeatable; assets without a `path` never match.", + "description": "Return only table assets whose `path` equals one of these paths or is nested under one of them (`/`-separated, e.g. `models/marts` matches `models/marts/orders.sql`; a trailing `/` is ignored). Repeatable; assets without a `path` never match.", "title": "Path" } } @@ -2464,7 +2464,7 @@ } }, { - "description": "Return only assets whose `path` equals one of these paths or is nested under one of them (`/`-separated, e.g. `models/marts` matches `models/marts/orders.sql`). Repeatable; assets without a `path` never match.", + "description": "Return only assets whose `path` equals one of these paths or is nested under one of them (`/`-separated, e.g. `models/marts` matches `models/marts/orders.sql`; a trailing `/` is ignored). Repeatable; assets without a `path` never match.", "in": "query", "name": "path", "required": false, @@ -2481,7 +2481,7 @@ "type": "null" } ], - "description": "Return only assets whose `path` equals one of these paths or is nested under one of them (`/`-separated, e.g. `models/marts` matches `models/marts/orders.sql`). Repeatable; assets without a `path` never match.", + "description": "Return only assets whose `path` equals one of these paths or is nested under one of them (`/`-separated, e.g. `models/marts` matches `models/marts/orders.sql`; a trailing `/` is ignored). Repeatable; assets without a `path` never match.", "title": "Path" } }, @@ -2795,7 +2795,7 @@ } }, { - "description": "Return only BI assets whose `path` equals one of these paths or is nested under one of them (`/`-separated, e.g. `models/marts` matches `models/marts/orders.sql`). Repeatable; assets without a `path` never match.", + "description": "Return only BI assets whose `path` equals one of these paths or is nested under one of them (`/`-separated, e.g. `models/marts` matches `models/marts/orders.sql`; a trailing `/` is ignored). Repeatable; assets without a `path` never match.", "in": "query", "name": "path", "required": false, @@ -2812,7 +2812,7 @@ "type": "null" } ], - "description": "Return only BI assets whose `path` equals one of these paths or is nested under one of them (`/`-separated, e.g. `models/marts` matches `models/marts/orders.sql`). Repeatable; assets without a `path` never match.", + "description": "Return only BI assets whose `path` equals one of these paths or is nested under one of them (`/`-separated, e.g. `models/marts` matches `models/marts/orders.sql`; a trailing `/` is ignored). Repeatable; assets without a `path` never match.", "title": "Path" } }, @@ -3310,7 +3310,7 @@ } }, { - "description": "Return only table assets whose `path` equals one of these paths or is nested under one of them (`/`-separated, e.g. `models/marts` matches `models/marts/orders.sql`). Repeatable; assets without a `path` never match.", + "description": "Return only table assets whose `path` equals one of these paths or is nested under one of them (`/`-separated, e.g. `models/marts` matches `models/marts/orders.sql`; a trailing `/` is ignored). Repeatable; assets without a `path` never match.", "in": "query", "name": "path", "required": false, @@ -3327,7 +3327,7 @@ "type": "null" } ], - "description": "Return only table assets whose `path` equals one of these paths or is nested under one of them (`/`-separated, e.g. `models/marts` matches `models/marts/orders.sql`). Repeatable; assets without a `path` never match.", + "description": "Return only table assets whose `path` equals one of these paths or is nested under one of them (`/`-separated, e.g. `models/marts` matches `models/marts/orders.sql`; a trailing `/` is ignored). Repeatable; assets without a `path` never match.", "title": "Path" } }, diff --git a/docs/api/introduction.mdx b/docs/api/introduction.mdx index 1a50acfc8..1b8843e2d 100644 --- a/docs/api/introduction.mdx +++ b/docs/api/introduction.mdx @@ -176,14 +176,15 @@ curl -G "https://prod.api.elementary-data.com/public/beta/$ENV_ID/asset-health-s } ``` -- Only [table assets](/api/reference/assets/list-table-assets) are included. - Filter them with the same parameters as `GET /assets/tables`: `ids`, +- Filters select [table assets](/api/reference/assets/list-table-assets), + using the same parameters as `GET /assets/tables`: `ids`, `source_types`, `tags`, `db_names`, `schema_names`, `materializations`, and `path`. Values within one parameter are OR-ed; different parameters are - AND-ed. With no filters, the score covers every monitored table you can see. + AND-ed. With no filters, the score covers every scored asset you can view. - `path` selects a folder: it matches assets whose `path` equals the value or is nested under it (`models/marts` matches `models/marts/orders.sql` but not - `models/marts_legacy/orders.sql`). Repeat it to combine folders. The same + `models/marts_legacy/orders.sql`). A trailing `/` is ignored, and `.sql` is + not stripped. Repeat it to combine folders. The same `path` filter is also available on `GET /assets`, `/assets/tables`, and `/assets/bi`. - Same scoring as above: scores are in the range `0`–`1`, computed from test From 739e0aac2d3efb7ab02b173e7696ec03e531c792 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 28 Sep 2026 15:29:28 +0000 Subject: [PATCH 3/8] docs(api): sync aggregate endpoint description (APP-1713) Co-Authored-By: mika@elementary-data.com --- docs/api-reference/beta/openapi.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-reference/beta/openapi.json b/docs/api-reference/beta/openapi.json index f19ce2304..67c8b3aae 100644 --- a/docs/api-reference/beta/openapi.json +++ b/docs/api-reference/beta/openapi.json @@ -1777,7 +1777,7 @@ }, "/public/beta/{env_id}/asset-health-scores/aggregate": { "get": { - "description": "Returns one health score card for the set of table assets matching the filters \u2014 the same number the Data Health dashboard shows for a filter or folder. Filters match `GET /assets/tables` plus `path` and intersect; with no filters, every scored asset you can view is included. The score is aggregated across the matching assets' test results in the last 7 days, not averaged from per-asset scores. When no matching asset has test results in the window, returns a zero card (`score` 0, `test_count` 0, `bucket_at` null, no dimensions).", + "description": "Returns one health score card for a set of assets \u2014 the same number the Data Health dashboard shows for a filter or folder. Filters match `GET /assets/tables` plus `path`, intersect, and restrict the set to table assets; with no filters, every scored asset you can view is included, of any kind and including assets not yet synced. The score is aggregated across the matching assets' test results in the last 7 days, not averaged from per-asset scores. When no matching asset has test results in the window, returns a zero card (`score` 0, `test_count` 0, `bucket_at` null, no dimensions).", "operationId": "aggregate_asset_health_scores_public_beta__env_id__asset_health_scores_aggregate_get", "parameters": [ { From 39b3538feb4976927063985b2c4ec08970d382c1 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 29 Sep 2026 09:02:50 +0000 Subject: [PATCH 4/8] docs(api): table-only membership, ids 400, matched_count on aggregate (APP-1713) Co-Authored-By: mika@elementary-data.com --- docs/api-reference/beta/openapi.json | 13 +++++++++---- docs/api/introduction.mdx | 16 ++++++++++++---- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/docs/api-reference/beta/openapi.json b/docs/api-reference/beta/openapi.json index 67c8b3aae..4d42f5006 100644 --- a/docs/api-reference/beta/openapi.json +++ b/docs/api-reference/beta/openapi.json @@ -11,12 +11,17 @@ "title": "Dimensions", "type": "array" }, + "matched_count": { + "description": "Number of table assets you can view that match the filters, whether or not they have test results in the window. `0` when nothing matches.", + "title": "Matched Count", + "type": "integer" + }, "total": { "$ref": "#/components/schemas/AggregatedHealthScoreBucket", "description": "Weighted score across all dimensions for the whole matching asset set, using the environment's configured dimension weights. Aggregated across the assets' test results, not an average of per-asset scores." } }, - "required": ["total", "dimensions"], + "required": ["matched_count", "total", "dimensions"], "title": "AggregatedHealthScore", "type": "object" }, @@ -1594,7 +1599,7 @@ }, "/public/beta/{env_id}/asset-health-scores": { "get": { - "description": "Lists the current health score of every asset that has test results in the last 7 days \u2014 a full snapshot, recomputed hourly. Not an incremental feed: re-read the snapshot instead of tracking changes. Ordered by `asset_id`.", + "description": "Lists the current health score of every table asset that has test results in the last 7 days \u2014 a full snapshot, recomputed hourly. Not an incremental feed: re-read the snapshot instead of tracking changes. Ordered by `asset_id`.", "operationId": "list_asset_health_scores_public_beta__env_id__asset_health_scores_get", "parameters": [ { @@ -1777,7 +1782,7 @@ }, "/public/beta/{env_id}/asset-health-scores/aggregate": { "get": { - "description": "Returns one health score card for a set of assets \u2014 the same number the Data Health dashboard shows for a filter or folder. Filters match `GET /assets/tables` plus `path`, intersect, and restrict the set to table assets; with no filters, every scored asset you can view is included, of any kind and including assets not yet synced. The score is aggregated across the matching assets' test results in the last 7 days, not averaged from per-asset scores. When no matching asset has test results in the window, returns a zero card (`score` 0, `test_count` 0, `bucket_at` null, no dimensions).", + "description": "Returns one health score card for a set of table assets \u2014 the same number the Data Health dashboard shows for a filter or folder. Filters match `GET /assets/tables` plus `path` and intersect; with no filters, every table asset you can view is included. Every `ids` value must be a table asset, otherwise the request fails with 400 listing the invalid ids. The score is aggregated across the matching assets' test results in the last 7 days, not averaged from per-asset scores. When no matching asset has test results in the window, returns a zero card (`score` 0, `test_count` 0, `bucket_at` null, no dimensions) alongside `matched_count`.", "operationId": "aggregate_asset_health_scores_public_beta__env_id__asset_health_scores_aggregate_get", "parameters": [ { @@ -2060,7 +2065,7 @@ }, "/public/beta/{env_id}/asset-health-scores/{asset_id}": { "get": { - "description": "Fetches the current health score of a single asset. Returns 404 if the asset doesn't exist in this environment or has no test results in the last 7 days.", + "description": "Fetches the current health score of a single table asset. Returns 404 if the id isn't a table asset in this environment or has no test results in the last 7 days.", "operationId": "get_asset_health_score_public_beta__env_id__asset_health_scores__asset_id__get", "parameters": [ { diff --git a/docs/api/introduction.mdx b/docs/api/introduction.mdx index 1b8843e2d..b9e730be9 100644 --- a/docs/api/introduction.mdx +++ b/docs/api/introduction.mdx @@ -122,7 +122,7 @@ verdict are not in this cut. ## Asset health scores **[`GET /asset-health-scores`](/api/reference/asset-health-scores/list-asset-health-scores)** -returns the current data health score of every monitored asset, the same number +returns the current data health score of every monitored table asset, the same number shown in the Elementary data health dashboard when looking at a single asset. Use it to feed scorecards or nightly quality reports. @@ -146,6 +146,9 @@ Each item has a `total` score and one entry per quality `dimension` buckets; `bucket_at` is the start of the most recent bucket with results. Dimensions with no results in that window are omitted, and assets with no results at all are not returned. +- Only [table assets](/api/reference/assets/list-table-assets) have health + scores. `GET /asset-health-scores/{asset_id}` returns `404` for a BI asset or + an id that is not a synced table. - Full snapshot, replaced in place — re-read it; there is no incremental feed. The only filter is `asset_ids`. Join to [assets](/api/reference/assets/list-assets) on `asset_id`. @@ -169,6 +172,7 @@ curl -G "https://prod.api.elementary-data.com/public/beta/$ENV_ID/asset-health-s ```json { + "matched_count": 40, "total": { "score": 0.91, "test_count": 12, "bucket_at": "2026-09-14T08:00:00" }, "dimensions": [ { "dimension": "freshness", "score": 0.99, "test_count": 3, "bucket_at": "2026-09-14T08:00:00" } @@ -176,11 +180,15 @@ curl -G "https://prod.api.elementary-data.com/public/beta/$ENV_ID/asset-health-s } ``` +- `matched_count` is the number of table assets you can view that match the + filters, including those with no test results in the window. - Filters select [table assets](/api/reference/assets/list-table-assets), using the same parameters as `GET /assets/tables`: `ids`, `source_types`, `tags`, `db_names`, `schema_names`, `materializations`, and `path`. Values within one parameter are OR-ed; different parameters are - AND-ed. With no filters, the score covers every scored asset you can view. + AND-ed. With no filters, the score covers every table asset you can view. +- Every `ids` value must be a table asset; if any is a BI asset or unknown, the + request fails with `400` and the error message lists the invalid ids. - `path` selects a folder: it matches assets whose `path` equals the value or is nested under it (`models/marts` matches `models/marts/orders.sql` but not `models/marts_legacy/orders.sql`). A trailing `/` is ignored, and `.sql` is @@ -191,8 +199,8 @@ curl -G "https://prod.api.elementary-data.com/public/beta/$ENV_ID/asset-health-s results in a fixed **last 7 days** window, and dimensions with no results are omitted. - If no matching asset has test results in the window, the response is still - `200` with a zero score: - `{"total": {"score": 0, "test_count": 0, "bucket_at": null}, "dimensions": []}`. + `200` with a zero score and the `matched_count` (`0` for an empty folder): + `{"matched_count": 0, "total": {"score": 0, "test_count": 0, "bucket_at": null}, "dimensions": []}`. ## Forward compatibility From 67becb544355f3a2d8f95daa9c15c8a774c3e0cc Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 29 Sep 2026 09:13:37 +0000 Subject: [PATCH 5/8] docs(api): sync aggregate endpoint description (APP-1713) Co-Authored-By: mika@elementary-data.com --- docs/api-reference/beta/openapi.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-reference/beta/openapi.json b/docs/api-reference/beta/openapi.json index 4d42f5006..639396f96 100644 --- a/docs/api-reference/beta/openapi.json +++ b/docs/api-reference/beta/openapi.json @@ -1782,7 +1782,7 @@ }, "/public/beta/{env_id}/asset-health-scores/aggregate": { "get": { - "description": "Returns one health score card for a set of table assets \u2014 the same number the Data Health dashboard shows for a filter or folder. Filters match `GET /assets/tables` plus `path` and intersect; with no filters, every table asset you can view is included. Every `ids` value must be a table asset, otherwise the request fails with 400 listing the invalid ids. The score is aggregated across the matching assets' test results in the last 7 days, not averaged from per-asset scores. When no matching asset has test results in the window, returns a zero card (`score` 0, `test_count` 0, `bucket_at` null, no dimensions) alongside `matched_count`.", + "description": "Returns one health score card for a set of table assets \u2014 the same number the Data Health dashboard shows for a filter or folder. The `ids`, `source_types`, `tags`, `db_names`, `schema_names`, `materializations` and `path` filters intersect; with no filters, every table asset you can view is included. Every `ids` value must be a table asset, otherwise the request fails with 400 listing the invalid ids. The score is aggregated across the matching assets' test results in the last 7 days, not averaged from per-asset scores. When no matching asset has test results in the window, returns a zero card (`score` 0, `test_count` 0, `bucket_at` null, no dimensions) alongside `matched_count`.", "operationId": "aggregate_asset_health_scores_public_beta__env_id__asset_health_scores_aggregate_get", "parameters": [ { From 93053a7fe043b4abe5f16e4888198195d497d9f9 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 29 Sep 2026 09:21:15 +0000 Subject: [PATCH 6/8] docs(api): list aggregate filters instead of claiming /assets/tables parity (APP-1713) Co-Authored-By: mika@elementary-data.com --- docs/api/introduction.mdx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/api/introduction.mdx b/docs/api/introduction.mdx index b9e730be9..b11bd8f84 100644 --- a/docs/api/introduction.mdx +++ b/docs/api/introduction.mdx @@ -182,10 +182,9 @@ curl -G "https://prod.api.elementary-data.com/public/beta/$ENV_ID/asset-health-s - `matched_count` is the number of table assets you can view that match the filters, including those with no test results in the window. -- Filters select [table assets](/api/reference/assets/list-table-assets), - using the same parameters as `GET /assets/tables`: `ids`, - `source_types`, `tags`, `db_names`, `schema_names`, `materializations`, and - `path`. Values within one parameter are OR-ed; different parameters are +- Filters select [table assets](/api/reference/assets/list-table-assets). The + supported filters are `ids`, `source_types`, `tags`, `db_names`, + `schema_names`, `materializations`, and `path`. Values within one parameter are OR-ed; different parameters are AND-ed. With no filters, the score covers every table asset you can view. - Every `ids` value must be a table asset; if any is a BI asset or unknown, the request fails with `400` and the error message lists the invalid ids. From 96cb2026bd2f123e8007e3770efc9ba69fca9283 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 29 Sep 2026 15:33:29 +0000 Subject: [PATCH 7/8] docs(api): clarify aggregate score excludes tables without results (APP-1713) Co-Authored-By: mika@elementary-data.com --- docs/api/introduction.mdx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/api/introduction.mdx b/docs/api/introduction.mdx index b11bd8f84..d624d6bf4 100644 --- a/docs/api/introduction.mdx +++ b/docs/api/introduction.mdx @@ -184,8 +184,12 @@ curl -G "https://prod.api.elementary-data.com/public/beta/$ENV_ID/asset-health-s filters, including those with no test results in the window. - Filters select [table assets](/api/reference/assets/list-table-assets). The supported filters are `ids`, `source_types`, `tags`, `db_names`, - `schema_names`, `materializations`, and `path`. Values within one parameter are OR-ed; different parameters are - AND-ed. With no filters, the score covers every table asset you can view. + `schema_names`, `materializations`, and `path`. Values within one parameter + are OR-ed; different parameters are AND-ed. With no filters, the call covers + every table asset in the environment that you can view. +- `total` and `dimensions` are computed only from matching tables that have test + results in the window; tables without results count toward `matched_count` + but not toward the score. - Every `ids` value must be a table asset; if any is a BI asset or unknown, the request fails with `400` and the error message lists the invalid ids. - `path` selects a folder: it matches assets whose `path` equals the value or is From e19b54d99b737c3ce981ab292871f15ca7f9204f Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 29 Sep 2026 19:40:15 +0000 Subject: [PATCH 8/8] docs(api): document path filter on asset endpoints; table-only health row (APP-1713) Co-Authored-By: mika@elementary-data.com --- docs/api/introduction.mdx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/api/introduction.mdx b/docs/api/introduction.mdx index d624d6bf4..511e459a2 100644 --- a/docs/api/introduction.mdx +++ b/docs/api/introduction.mdx @@ -65,7 +65,7 @@ Follow `next_cursor` until `has_more` is `false` — see [Pagination](/api/pagin | Tests | `GET /{env_id}/tests` | Test definitions | | Latest test executions | `GET /{env_id}/latest-test-executions` | Current run per sub-test | | Test execution history | `GET /{env_id}/tests/{test_id}/executions` | History for one test | -| Asset health scores | `GET /{env_id}/asset-health-scores` | Current health score per asset | +| Asset health scores | `GET /{env_id}/asset-health-scores` | Current health score per table asset | | Aggregate health score | `GET /{env_id}/asset-health-scores/aggregate` | One health score for a filtered set of tables | Every list endpoint supports a full scan and keyset pagination. Assets, @@ -90,6 +90,10 @@ fields but each kind also has its own attributes, so the API splits them: `materialization`) for tables, and `bi_platform` / `bi_type` / `url` for BI assets. Kind-specific filters live here too (e.g. `db_names` on `/assets/tables`, `bi_platforms` on `/assets/bi`). +- All three accept a **`path`** filter to select a folder: it matches assets + whose `path` equals the value or is nested under it (`models/marts` matches + `models/marts/orders.sql` but not `models/marts_legacy/orders.sql`). A + trailing `/` is ignored; repeat the parameter to combine folders. **Why the split?** A single asset shape would leave most fields null on any given row (a BI dashboard has no `db_name`; a table has no `bi_platform`). Keeping the