From 15ffb7a70300490e3e9d647d36f4e45c599c8fb4 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 12 May 2026 17:09:58 -0700 Subject: [PATCH 1/5] improvement(grafana): align tools and block with official Grafana API spec Validates and corrects the Grafana integration against the official API docs: fixes wire-format field naming for provisioned alert rules (missing_series_evals_to_resolve, keepFiringFor, orgID), adds X-Disable-Provenance support, expands alert-rule params (isPaused, notificationSettings, record, annotations, labels), corrects defaults (execErrState=Error, dashboard overwrite=false), and centralizes alert-rule output mapping in a shared utils module. Co-Authored-By: Claude Opus 4.7 --- apps/docs/content/docs/en/tools/grafana.mdx | 180 +++++++++--- apps/sim/blocks/blocks/grafana.ts | 276 ++++++++++++++++-- apps/sim/tools/grafana/create_alert_rule.ts | 132 +++++---- apps/sim/tools/grafana/create_annotation.ts | 25 +- apps/sim/tools/grafana/create_folder.ts | 83 +++--- apps/sim/tools/grafana/get_alert_rule.ts | 68 +---- apps/sim/tools/grafana/get_data_source.ts | 97 +++--- apps/sim/tools/grafana/list_alert_rules.ts | 31 +- apps/sim/tools/grafana/list_annotations.ts | 97 ++++-- apps/sim/tools/grafana/list_contact_points.ts | 36 ++- apps/sim/tools/grafana/list_dashboards.ts | 56 ++-- apps/sim/tools/grafana/list_data_sources.ts | 59 +++- apps/sim/tools/grafana/list_folders.ts | 98 +++++-- apps/sim/tools/grafana/types.ts | 150 ++++++---- apps/sim/tools/grafana/update_alert_rule.ts | 106 ++++--- apps/sim/tools/grafana/update_annotation.ts | 20 +- apps/sim/tools/grafana/update_dashboard.ts | 5 +- apps/sim/tools/grafana/utils.ts | 75 +++++ 18 files changed, 1077 insertions(+), 517 deletions(-) create mode 100644 apps/sim/tools/grafana/utils.ts diff --git a/apps/docs/content/docs/en/tools/grafana.mdx b/apps/docs/content/docs/en/tools/grafana.mdx index f3d0b63208a..c09e2335fb4 100644 --- a/apps/docs/content/docs/en/tools/grafana.mdx +++ b/apps/docs/content/docs/en/tools/grafana.mdx @@ -71,9 +71,11 @@ Search and list all dashboards | `organizationId` | string | No | Organization ID for multi-org Grafana instances \(e.g., 1, 2\) | | `query` | string | No | Search query to filter dashboards by title | | `tag` | string | No | Filter by tag \(comma-separated for multiple tags\) | -| `folderIds` | string | No | Filter by folder IDs \(comma-separated, e.g., 1,2,3\) | +| `folderUIDs` | string | No | Filter by folder UIDs \(comma-separated, e.g., abc123,def456\) | +| `dashboardUIDs` | string | No | Filter by dashboard UIDs \(comma-separated, e.g., abc123,def456\) | | `starred` | boolean | No | Only return starred dashboards | -| `limit` | number | No | Maximum number of dashboards to return | +| `limit` | number | No | Maximum number of dashboards to return \(default 1000\) | +| `page` | number | No | Page number for pagination \(1-based\) | #### Output @@ -136,7 +138,7 @@ Update an existing dashboard. Fetches the current dashboard and merges your chan | `timezone` | string | No | Dashboard timezone \(e.g., browser, utc\) | | `refresh` | string | No | Auto-refresh interval \(e.g., 5s, 1m, 5m\) | | `panels` | string | No | JSON array of panel configurations | -| `overwrite` | boolean | No | Overwrite even if there is a version conflict | +| `overwrite` | boolean | No | Overwrite even if there is a version conflict \(defaults to false to surface 412 conflicts\) | | `message` | string | No | Commit message for this version | #### Output @@ -188,13 +190,6 @@ List all alert rules in the Grafana instance | Parameter | Type | Description | | --------- | ---- | ----------- | | `rules` | array | List of alert rules | -| ↳ `uid` | string | Alert rule UID | -| ↳ `title` | string | Alert rule title | -| ↳ `condition` | string | Alert condition | -| ↳ `folderUID` | string | Parent folder UID | -| ↳ `ruleGroup` | string | Rule group name | -| ↳ `noDataState` | string | State when no data is returned | -| ↳ `execErrState` | string | State on execution error | ### `grafana_get_alert_rule` @@ -213,16 +208,35 @@ Get a specific alert rule by its UID | Parameter | Type | Description | | --------- | ---- | ----------- | -| `uid` | string | Alert rule UID | -| `title` | string | Alert rule title | -| `condition` | string | Alert condition | -| `data` | json | Alert rule query data | +| `version` | string | Grafana version | +| `database` | string | Database health status | +| `status` | string | Health status | +| `dashboard` | json | Dashboard JSON | +| `meta` | json | Dashboard metadata | +| `dashboards` | json | List of dashboards | +| `uid` | string | Created/updated UID | +| `url` | string | Dashboard URL | +| `rules` | json | Alert rules list | +| `contactPoints` | json | Contact points list | +| `condition` | string | Alert condition refId | +| `for` | string | Duration the condition must hold before firing | +| `keepFiringFor` | string | Duration to keep firing after the condition stops | +| `missingSeriesEvalsToResolve` | number | Missing series evaluations before resolving | +| `isPaused` | boolean | Whether the alert rule is paused | | `folderUID` | string | Parent folder UID | | `ruleGroup` | string | Rule group name | -| `noDataState` | string | State when no data is returned | +| `orgID` | number | Organization ID | +| `provenance` | string | Provisioning source | +| `noDataState` | string | State on no data | | `execErrState` | string | State on execution error | -| `annotations` | json | Alert annotations | -| `labels` | json | Alert labels | +| `notification_settings` | json | Per-rule notification settings | +| `record` | json | Recording rule configuration | +| `updated` | string | Last update timestamp | +| `annotations` | json | Annotations list | +| `id` | number | Annotation ID | +| `dataSources` | json | Data sources list | +| `folders` | json | Folders list | +| `message` | string | Status message | ### `grafana_create_alert_rule` @@ -238,22 +252,54 @@ Create a new alert rule | `title` | string | Yes | The title of the alert rule | | `folderUid` | string | Yes | The UID of the folder to create the alert in \(e.g., folder-abc123\) | | `ruleGroup` | string | Yes | The name of the rule group | -| `condition` | string | Yes | The refId of the query or expression to use as the alert condition | +| `condition` | string | No | The refId of the query or expression to use as the alert condition \(required for alerting rules; omit for recording rules\) | | `data` | string | Yes | JSON array of query/expression data objects | | `forDuration` | string | No | Duration to wait before firing \(e.g., 5m, 1h\) | | `noDataState` | string | No | State when no data is returned \(NoData, Alerting, OK\) | -| `execErrState` | string | No | State on execution error \(Alerting, OK\) | +| `execErrState` | string | No | State on execution error \(Error, Alerting, OK\) | | `annotations` | string | No | JSON object of annotations | | `labels` | string | No | JSON object of labels | +| `uid` | string | No | Optional custom UID for the alert rule | +| `isPaused` | boolean | No | Whether the rule is paused on creation | +| `keepFiringFor` | string | No | Duration to keep firing after the condition stops \(e.g., 5m\) | +| `missingSeriesEvalsToResolve` | number | No | Number of missing series evaluations before resolving | +| `notificationSettings` | string | No | JSON object of per-rule notification settings \(overrides\) | +| `record` | string | No | JSON object configuring this as a recording rule \(omit for alerting rules\) | +| `disableProvenance` | boolean | No | Set X-Disable-Provenance header so the rule remains editable in the Grafana UI | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `uid` | string | The UID of the created alert rule | -| `title` | string | Alert rule title | +| `version` | string | Grafana version | +| `database` | string | Database health status | +| `status` | string | Health status | +| `dashboard` | json | Dashboard JSON | +| `meta` | json | Dashboard metadata | +| `dashboards` | json | List of dashboards | +| `uid` | string | Created/updated UID | +| `url` | string | Dashboard URL | +| `rules` | json | Alert rules list | +| `contactPoints` | json | Contact points list | +| `condition` | string | Alert condition refId | +| `for` | string | Duration the condition must hold before firing | +| `keepFiringFor` | string | Duration to keep firing after the condition stops | +| `missingSeriesEvalsToResolve` | number | Missing series evaluations before resolving | +| `isPaused` | boolean | Whether the alert rule is paused | | `folderUID` | string | Parent folder UID | | `ruleGroup` | string | Rule group name | +| `orgID` | number | Organization ID | +| `provenance` | string | Provisioning source | +| `noDataState` | string | State on no data | +| `execErrState` | string | State on execution error | +| `notification_settings` | json | Per-rule notification settings | +| `record` | json | Recording rule configuration | +| `updated` | string | Last update timestamp | +| `annotations` | json | Annotations list | +| `id` | number | Annotation ID | +| `dataSources` | json | Data sources list | +| `folders` | json | Folders list | +| `message` | string | Status message | ### `grafana_update_alert_rule` @@ -277,15 +323,46 @@ Update an existing alert rule. Fetches the current rule and merges your changes. | `execErrState` | string | No | State on execution error \(Alerting, OK\) | | `annotations` | string | No | JSON object of annotations | | `labels` | string | No | JSON object of labels | +| `isPaused` | boolean | No | Whether the rule is paused | +| `keepFiringFor` | string | No | Duration to keep firing after the condition stops \(e.g., 5m\) | +| `missingSeriesEvalsToResolve` | number | No | Number of missing series evaluations before resolving | +| `notificationSettings` | string | No | JSON object of per-rule notification settings \(overrides\) | +| `record` | string | No | JSON object configuring this as a recording rule | +| `disableProvenance` | boolean | No | Set X-Disable-Provenance header so the rule remains editable in the Grafana UI | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `uid` | string | The UID of the updated alert rule | -| `title` | string | Alert rule title | +| `version` | string | Grafana version | +| `database` | string | Database health status | +| `status` | string | Health status | +| `dashboard` | json | Dashboard JSON | +| `meta` | json | Dashboard metadata | +| `dashboards` | json | List of dashboards | +| `uid` | string | Created/updated UID | +| `url` | string | Dashboard URL | +| `rules` | json | Alert rules list | +| `contactPoints` | json | Contact points list | +| `condition` | string | Alert condition refId | +| `for` | string | Duration the condition must hold before firing | +| `keepFiringFor` | string | Duration to keep firing after the condition stops | +| `missingSeriesEvalsToResolve` | number | Missing series evaluations before resolving | +| `isPaused` | boolean | Whether the alert rule is paused | | `folderUID` | string | Parent folder UID | | `ruleGroup` | string | Rule group name | +| `orgID` | number | Organization ID | +| `provenance` | string | Provisioning source | +| `noDataState` | string | State on no data | +| `execErrState` | string | State on execution error | +| `notification_settings` | json | Per-rule notification settings | +| `record` | json | Recording rule configuration | +| `updated` | string | Last update timestamp | +| `annotations` | json | Annotations list | +| `id` | number | Annotation ID | +| `dataSources` | json | Data sources list | +| `folders` | json | Folders list | +| `message` | string | Status message | ### `grafana_delete_alert_rule` @@ -317,6 +394,7 @@ List all alert notification contact points | `apiKey` | string | Yes | Grafana Service Account Token | | `baseUrl` | string | Yes | Grafana instance URL \(e.g., https://your-grafana.com\) | | `organizationId` | string | No | Organization ID for multi-org Grafana instances \(e.g., 1, 2\) | +| `name` | string | No | Filter contact points by exact name match | #### Output @@ -327,6 +405,8 @@ List all alert notification contact points | ↳ `name` | string | Contact point name | | ↳ `type` | string | Notification type \(email, slack, etc.\) | | ↳ `settings` | object | Type-specific settings | +| ↳ `disableResolveMessage` | boolean | Whether resolve messages are disabled | +| ↳ `provenance` | string | Provisioning source \(empty if API-managed\) | ### `grafana_create_annotation` @@ -341,7 +421,7 @@ Create an annotation on a dashboard or as a global annotation | `organizationId` | string | No | Organization ID for multi-org Grafana instances \(e.g., 1, 2\) | | `text` | string | Yes | The text content of the annotation | | `tags` | string | No | Comma-separated list of tags | -| `dashboardUid` | string | Yes | UID of the dashboard to add the annotation to \(e.g., abc123def\) | +| `dashboardUid` | string | No | UID of the dashboard to add the annotation to \(e.g., abc123def\). Omit to create a global organization annotation. | | `panelId` | number | No | ID of the panel to add the annotation to \(e.g., 1, 2\) | | `time` | number | No | Start time in epoch milliseconds \(e.g., 1704067200000, defaults to now\) | | `timeEnd` | number | No | End time in epoch milliseconds for range annotations \(e.g., 1704153600000\) | @@ -366,8 +446,11 @@ Query annotations by time range, dashboard, or tags | `organizationId` | string | No | Organization ID for multi-org Grafana instances \(e.g., 1, 2\) | | `from` | number | No | Start time in epoch milliseconds \(e.g., 1704067200000\) | | `to` | number | No | End time in epoch milliseconds \(e.g., 1704153600000\) | -| `dashboardUid` | string | Yes | Dashboard UID to query annotations from \(e.g., abc123def\) | +| `dashboardUid` | string | No | Dashboard UID to query annotations from \(e.g., abc123def\). Omit to query annotations across the organization. | +| `dashboardId` | number | No | Legacy numeric dashboard ID filter \(prefer dashboardUid\) | | `panelId` | number | No | Filter by panel ID \(e.g., 1, 2\) | +| `alertId` | number | No | Filter by alert ID | +| `userId` | number | No | Filter by ID of the user who created the annotation | | `tags` | string | No | Comma-separated list of tags to filter by | | `type` | string | No | Filter by type \(alert or annotation\) | | `limit` | number | No | Maximum number of annotations to return | @@ -378,17 +461,19 @@ Query annotations by time range, dashboard, or tags | --------- | ---- | ----------- | | `annotations` | array | List of annotations | | ↳ `id` | number | Annotation ID | +| ↳ `alertId` | number | Associated alert ID \(0 if not alert-driven\) | | ↳ `dashboardId` | number | Dashboard ID | | ↳ `dashboardUID` | string | Dashboard UID | -| ↳ `created` | number | Creation timestamp in epoch ms | -| ↳ `updated` | number | Last update timestamp in epoch ms | +| ↳ `panelId` | number | Panel ID within the dashboard | +| ↳ `userId` | number | ID of the user who created the annotation | +| ↳ `userName` | string | Username of the user who created the annotation | +| ↳ `newState` | string | New alert state \(alert annotations only\) | +| ↳ `prevState` | string | Previous alert state \(alert annotations only\) | | ↳ `time` | number | Start time in epoch ms | | ↳ `timeEnd` | number | End time in epoch ms | | ↳ `text` | string | Annotation text | +| ↳ `metric` | string | Metric associated with the annotation | | ↳ `tags` | array | Annotation tags | -| ↳ `login` | string | Login of the user who created the annotation | -| ↳ `email` | string | Email of the user who created the annotation | -| ↳ `avatarUrl` | string | Avatar URL of the user | | ↳ `data` | json | Additional annotation data object from Grafana | ### `grafana_update_annotation` @@ -403,7 +488,7 @@ Update an existing annotation | `baseUrl` | string | Yes | Grafana instance URL \(e.g., https://your-grafana.com\) | | `organizationId` | string | No | Organization ID for multi-org Grafana instances \(e.g., 1, 2\) | | `annotationId` | number | Yes | The ID of the annotation to update | -| `text` | string | Yes | New text content for the annotation | +| `text` | string | No | New text content for the annotation \(PATCH supports partial updates\) | | `tags` | string | No | Comma-separated list of new tags | | `time` | number | No | New start time in epoch milliseconds \(e.g., 1704067200000\) | | `timeEnd` | number | No | New end time in epoch milliseconds \(e.g., 1704153600000\) | @@ -453,10 +538,22 @@ List all data sources configured in Grafana | `dataSources` | array | List of data sources | | ↳ `id` | number | Data source ID | | ↳ `uid` | string | Data source UID | +| ↳ `orgId` | number | Organization ID | | ↳ `name` | string | Data source name | | ↳ `type` | string | Data source type \(prometheus, mysql, etc.\) | +| ↳ `typeLogoUrl` | string | Logo URL for the data source type | +| ↳ `access` | string | Access mode \(proxy or direct\) | | ↳ `url` | string | Data source URL | +| ↳ `user` | string | Username used to connect | +| ↳ `database` | string | Database name \(if applicable\) | +| ↳ `basicAuth` | boolean | Whether basic auth is enabled | +| ↳ `basicAuthUser` | string | Basic auth username | +| ↳ `withCredentials` | boolean | Whether to send credentials with cross-origin requests | | ↳ `isDefault` | boolean | Whether this is the default data source | +| ↳ `jsonData` | object | Type-specific JSON configuration | +| ↳ `secureJsonFields` | object | Map of secure fields that are set \(values are not returned\) | +| ↳ `version` | number | Data source version | +| ↳ `readOnly` | boolean | Whether the data source is read-only | ### `grafana_get_data_source` @@ -477,12 +574,22 @@ Get a data source by its ID or UID | --------- | ---- | ----------- | | `id` | number | Data source ID | | `uid` | string | Data source UID | +| `orgId` | number | Organization ID | | `name` | string | Data source name | | `type` | string | Data source type | +| `typeLogoUrl` | string | Logo URL for the data source type | +| `access` | string | Access mode \(proxy or direct\) | | `url` | string | Data source connection URL | +| `user` | string | Username used to connect | | `database` | string | Database name \(if applicable\) | +| `basicAuth` | boolean | Whether basic auth is enabled | +| `basicAuthUser` | string | Basic auth username | +| `withCredentials` | boolean | Whether to send credentials with cross-origin requests | | `isDefault` | boolean | Whether this is the default data source | | `jsonData` | json | Additional data source configuration | +| `secureJsonFields` | object | Map of secure fields that are set \(values are not returned\) | +| `version` | number | Data source version | +| `readOnly` | boolean | Whether the data source is read-only | ### `grafana_list_folders` @@ -497,6 +604,7 @@ List all folders in Grafana | `organizationId` | string | No | Organization ID for multi-org Grafana instances \(e.g., 1, 2\) | | `limit` | number | No | Maximum number of folders to return | | `page` | number | No | Page number for pagination | +| `parentUid` | string | No | List children of this folder UID \(requires nested folders enabled\) | #### Output @@ -506,16 +614,18 @@ List all folders in Grafana | ↳ `id` | number | Folder ID | | ↳ `uid` | string | Folder UID | | ↳ `title` | string | Folder title | +| ↳ `url` | string | Folder URL path | +| ↳ `parentUid` | string | Parent folder UID \(nested folders only\) | +| ↳ `parents` | array | Ancestor folder hierarchy \(nested folders only\) | | ↳ `hasAcl` | boolean | Whether the folder has custom ACL permissions | | ↳ `canSave` | boolean | Whether the current user can save the folder | | ↳ `canEdit` | boolean | Whether the current user can edit the folder | | ↳ `canAdmin` | boolean | Whether the current user has admin rights | -| ↳ `canDelete` | boolean | Whether the current user can delete the folder | | ↳ `createdBy` | string | Username of who created the folder | | ↳ `created` | string | Timestamp when the folder was created | | ↳ `updatedBy` | string | Username of who last updated the folder | | ↳ `updated` | string | Timestamp when the folder was last updated | -| ↳ `version` | number | Version number of the folder | +| ↳ `version` | number | Folder version number | ### `grafana_create_folder` @@ -530,6 +640,7 @@ Create a new folder in Grafana | `organizationId` | string | No | Organization ID for multi-org Grafana instances \(e.g., 1, 2\) | | `title` | string | Yes | The title of the new folder | | `uid` | string | No | Optional UID for the folder \(auto-generated if not provided\) | +| `parentUid` | string | No | Parent folder UID for nested folders \(requires nested folders enabled\) | #### Output @@ -539,11 +650,12 @@ Create a new folder in Grafana | `uid` | string | The UID of the created folder | | `title` | string | The title of the created folder | | `url` | string | The URL path to the folder | +| `parentUid` | string | Parent folder UID \(nested folders only\) | +| `parents` | array | Ancestor folder hierarchy \(nested folders only\) | | `hasAcl` | boolean | Whether the folder has custom ACL permissions | | `canSave` | boolean | Whether the current user can save the folder | | `canEdit` | boolean | Whether the current user can edit the folder | | `canAdmin` | boolean | Whether the current user has admin rights on the folder | -| `canDelete` | boolean | Whether the current user can delete the folder | | `createdBy` | string | Username of who created the folder | | `created` | string | Timestamp when the folder was created | | `updatedBy` | string | Username of who last updated the folder | diff --git a/apps/sim/blocks/blocks/grafana.ts b/apps/sim/blocks/blocks/grafana.ts index 4ef7f36810a..1f65d255f79 100644 --- a/apps/sim/blocks/blocks/grafana.ts +++ b/apps/sim/blocks/blocks/grafana.ts @@ -126,6 +126,33 @@ Return ONLY the search query - no explanations, no quotes, no extra text.`, placeholder: 'tag1, tag2 (comma-separated)', condition: { field: 'operation', value: 'grafana_list_dashboards' }, }, + { + id: 'folderUIDs', + title: 'Folder UIDs', + type: 'short-input', + placeholder: 'uid1, uid2 (comma-separated)', + mode: 'advanced', + condition: { field: 'operation', value: 'grafana_list_dashboards' }, + }, + { + id: 'dashboardUIDs', + title: 'Dashboard UIDs', + type: 'short-input', + placeholder: 'uid1, uid2 (comma-separated)', + mode: 'advanced', + condition: { field: 'operation', value: 'grafana_list_dashboards' }, + }, + { + id: 'page', + title: 'Page', + type: 'short-input', + placeholder: 'Page number (1-based)', + mode: 'advanced', + condition: { + field: 'operation', + value: ['grafana_list_dashboards', 'grafana_list_folders'], + }, + }, // Create/Update Dashboard { @@ -156,13 +183,15 @@ Return ONLY the title - no explanations, no quotes, no extra text.`, id: 'folderUid', title: 'Folder UID', type: 'short-input', - placeholder: 'Optional - folder to create dashboard in', + placeholder: 'Folder UID (required for alert rules, optional for dashboards)', + required: { field: 'operation', value: 'grafana_create_alert_rule' }, condition: { field: 'operation', value: [ 'grafana_create_dashboard', 'grafana_update_dashboard', 'grafana_create_alert_rule', + 'grafana_update_alert_rule', ], }, }, @@ -229,6 +258,16 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`, value: ['grafana_create_dashboard', 'grafana_update_dashboard'], }, }, + { + id: 'overwrite', + title: 'Overwrite on Conflict', + type: 'switch', + mode: 'advanced', + condition: { + field: 'operation', + value: ['grafana_create_dashboard', 'grafana_update_dashboard'], + }, + }, // Alert Rule operations { @@ -268,16 +307,6 @@ Return ONLY the alert title - no explanations, no quotes, no extra text.`, placeholder: 'Describe the alert...', }, }, - { - id: 'folderUid', - title: 'Folder UID', - type: 'short-input', - placeholder: 'Folder UID for the alert rule', - condition: { - field: 'operation', - value: ['grafana_create_alert_rule', 'grafana_update_alert_rule'], - }, - }, { id: 'ruleGroup', title: 'Rule Group', @@ -380,10 +409,105 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`, title: 'Error State', type: 'dropdown', options: [ + { label: 'Error', id: 'Error' }, { label: 'Alerting', id: 'Alerting' }, { label: 'OK', id: 'OK' }, ], - value: () => 'Alerting', + value: () => 'Error', + condition: { + field: 'operation', + value: ['grafana_create_alert_rule', 'grafana_update_alert_rule'], + }, + }, + { + id: 'annotations', + title: 'Annotations (JSON)', + type: 'long-input', + placeholder: 'JSON object of alert annotations (e.g., {"summary":"..."})', + mode: 'advanced', + condition: { + field: 'operation', + value: ['grafana_create_alert_rule', 'grafana_update_alert_rule'], + }, + }, + { + id: 'labels', + title: 'Labels (JSON)', + type: 'long-input', + placeholder: 'JSON object of alert labels (e.g., {"severity":"critical"})', + mode: 'advanced', + condition: { + field: 'operation', + value: ['grafana_create_alert_rule', 'grafana_update_alert_rule'], + }, + }, + { + id: 'isPaused', + title: 'Paused', + type: 'switch', + mode: 'advanced', + condition: { + field: 'operation', + value: ['grafana_create_alert_rule', 'grafana_update_alert_rule'], + }, + }, + { + id: 'keepFiringFor', + title: 'Keep Firing For', + type: 'short-input', + placeholder: 'e.g., 5m', + mode: 'advanced', + condition: { + field: 'operation', + value: ['grafana_create_alert_rule', 'grafana_update_alert_rule'], + }, + }, + { + id: 'missingSeriesEvalsToResolve', + title: 'Missing Series Evals to Resolve', + type: 'short-input', + placeholder: 'e.g., 2', + mode: 'advanced', + condition: { + field: 'operation', + value: ['grafana_create_alert_rule', 'grafana_update_alert_rule'], + }, + }, + { + id: 'notificationSettings', + title: 'Notification Settings (JSON)', + type: 'long-input', + placeholder: 'JSON object of per-rule notification overrides', + mode: 'advanced', + condition: { + field: 'operation', + value: ['grafana_create_alert_rule', 'grafana_update_alert_rule'], + }, + }, + { + id: 'record', + title: 'Recording Rule (JSON)', + type: 'long-input', + placeholder: 'JSON object configuring this as a recording rule', + mode: 'advanced', + condition: { + field: 'operation', + value: ['grafana_create_alert_rule', 'grafana_update_alert_rule'], + }, + }, + { + id: 'alertRuleUidNew', + title: 'Custom Alert Rule UID', + type: 'short-input', + placeholder: 'Optional - auto-generated if not provided', + mode: 'advanced', + condition: { field: 'operation', value: 'grafana_create_alert_rule' }, + }, + { + id: 'disableProvenance', + title: 'Disable Provenance', + type: 'switch', + mode: 'advanced', condition: { field: 'operation', value: ['grafana_create_alert_rule', 'grafana_update_alert_rule'], @@ -396,7 +520,7 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`, title: 'Annotation Text', type: 'long-input', placeholder: 'Enter annotation text...', - required: true, + required: { field: 'operation', value: 'grafana_create_annotation' }, condition: { field: 'operation', value: ['grafana_create_annotation', 'grafana_update_annotation'], @@ -436,8 +560,7 @@ Return ONLY the annotation text - no explanations, no quotes, no extra text.`, id: 'annotationDashboardUid', title: 'Dashboard UID', type: 'short-input', - placeholder: 'Enter dashboard UID', - required: true, + placeholder: 'Optional - omit for organization-wide annotations', condition: { field: 'operation', value: ['grafana_create_annotation', 'grafana_list_annotations'], @@ -453,6 +576,22 @@ Return ONLY the annotation text - no explanations, no quotes, no extra text.`, value: ['grafana_create_annotation', 'grafana_list_annotations'], }, }, + { + id: 'alertId', + title: 'Alert ID', + type: 'short-input', + placeholder: 'Filter by alert ID', + mode: 'advanced', + condition: { field: 'operation', value: 'grafana_list_annotations' }, + }, + { + id: 'userId', + title: 'User ID', + type: 'short-input', + placeholder: 'Filter by creator user ID', + mode: 'advanced', + condition: { field: 'operation', value: 'grafana_list_annotations' }, + }, { id: 'time', title: 'Time (epoch ms)', @@ -583,6 +722,30 @@ Return ONLY the folder title - no explanations, no quotes, no extra text.`, placeholder: 'Optional - auto-generated if not provided', condition: { field: 'operation', value: 'grafana_create_folder' }, }, + { + id: 'parentUidNew', + title: 'Parent Folder UID', + type: 'short-input', + placeholder: 'Optional - for nested folders', + mode: 'advanced', + condition: { field: 'operation', value: 'grafana_create_folder' }, + }, + { + id: 'parentUidList', + title: 'Parent Folder UID', + type: 'short-input', + placeholder: 'List children of this folder UID', + mode: 'advanced', + condition: { field: 'operation', value: 'grafana_list_folders' }, + }, + { + id: 'contactPointName', + title: 'Contact Point Name', + type: 'short-input', + placeholder: 'Filter by exact name', + mode: 'advanced', + condition: { field: 'operation', value: 'grafana_list_contact_points' }, + }, ], tools: { access: [ @@ -607,22 +770,30 @@ Return ONLY the folder title - no explanations, no quotes, no extra text.`, 'grafana_create_folder', ], config: { - tool: (params) => { - if (params.alertTitle) params.title = params.alertTitle - if (params.folderTitle) params.title = params.folderTitle - if (params.folderUidNew) params.uid = params.folderUidNew - if (params.annotationTags) params.tags = params.annotationTags - if (params.annotationDashboardUid) params.dashboardUid = params.annotationDashboardUid - return params.operation - }, + tool: (params) => params.operation, params: (params) => { const result: Record = {} + if (params.alertTitle) result.title = params.alertTitle + if (params.folderTitle) result.title = params.folderTitle + if (params.folderUidNew) result.uid = params.folderUidNew + if (params.alertRuleUidNew) result.uid = params.alertRuleUidNew + if (params.parentUidNew) result.parentUid = params.parentUidNew + if (params.parentUidList) result.parentUid = params.parentUidList + if (params.contactPointName) result.name = params.contactPointName + if (params.annotationTags) result.tags = params.annotationTags + if (params.annotationDashboardUid) result.dashboardUid = params.annotationDashboardUid if (params.panelId) result.panelId = Number(params.panelId) if (params.annotationId) result.annotationId = Number(params.annotationId) + if (params.alertId) result.alertId = Number(params.alertId) + if (params.userId) result.userId = Number(params.userId) if (params.time) result.time = Number(params.time) if (params.timeEnd) result.timeEnd = Number(params.timeEnd) if (params.from) result.from = Number(params.from) if (params.to) result.to = Number(params.to) + if (params.page) result.page = Number(params.page) + if (params.missingSeriesEvalsToResolve) { + result.missingSeriesEvalsToResolve = Number(params.missingSeriesEvalsToResolve) + } return result }, }, @@ -641,8 +812,15 @@ Return ONLY the folder title - no explanations, no quotes, no extra text.`, message: { type: 'string', description: 'Commit message' }, query: { type: 'string', description: 'Search query' }, tag: { type: 'string', description: 'Filter by tag' }, + folderUIDs: { + type: 'string', + description: 'Filter dashboards by folder UIDs (comma-separated)', + }, + dashboardUIDs: { type: 'string', description: 'Filter by dashboard UIDs (comma-separated)' }, + page: { type: 'number', description: 'Page number for pagination' }, // Alert inputs alertRuleUid: { type: 'string', description: 'Alert rule UID' }, + alertRuleUidNew: { type: 'string', description: 'Custom UID for newly created alert rule' }, alertTitle: { type: 'string', description: 'Alert rule title' }, ruleGroup: { type: 'string', description: 'Rule group name' }, condition: { type: 'string', description: 'Alert condition refId' }, @@ -650,14 +828,46 @@ Return ONLY the folder title - no explanations, no quotes, no extra text.`, forDuration: { type: 'string', description: 'Duration before firing' }, noDataState: { type: 'string', description: 'State on no data' }, execErrState: { type: 'string', description: 'State on error' }, + isPaused: { type: 'boolean', description: 'Whether the alert rule is paused' }, + keepFiringFor: { + type: 'string', + description: 'Duration to keep firing after the condition stops', + }, + missingSeriesEvalsToResolve: { + type: 'number', + description: 'Missing series evaluations before resolving', + }, + notificationSettings: { + type: 'string', + description: 'JSON of per-rule notification settings', + }, + record: { type: 'string', description: 'JSON of recording rule configuration' }, + disableProvenance: { + type: 'boolean', + description: 'Disable provenance tracking so the rule remains UI-editable', + }, + annotations: { type: 'string', description: 'JSON of alert annotations' }, + labels: { type: 'string', description: 'JSON of alert labels' }, + overwrite: { type: 'boolean', description: 'Overwrite existing dashboard on version conflict' }, // Annotation inputs text: { type: 'string', description: 'Annotation text' }, annotationId: { type: 'number', description: 'Annotation ID' }, + annotationTags: { type: 'string', description: 'Annotation tags (comma-separated)' }, + annotationDashboardUid: { type: 'string', description: 'Annotation dashboard UID' }, panelId: { type: 'number', description: 'Panel ID' }, time: { type: 'number', description: 'Start time (epoch ms)' }, timeEnd: { type: 'number', description: 'End time (epoch ms)' }, from: { type: 'number', description: 'Filter from time' }, to: { type: 'number', description: 'Filter to time' }, + alertId: { type: 'number', description: 'Filter annotations by alert ID' }, + userId: { type: 'number', description: 'Filter annotations by creator user ID' }, + // Folder inputs + folderTitle: { type: 'string', description: 'Folder title for newly created folder' }, + folderUidNew: { type: 'string', description: 'Custom UID for newly created folder' }, + parentUidList: { type: 'string', description: 'Parent folder UID to list children of' }, + parentUidNew: { type: 'string', description: 'Parent folder UID for newly created folder' }, + // Contact point inputs + contactPointName: { type: 'string', description: 'Filter contact points by name' }, // Data source inputs dataSourceId: { type: 'string', description: 'Data source ID or UID' }, }, @@ -675,6 +885,26 @@ Return ONLY the folder title - no explanations, no quotes, no extra text.`, // Alert outputs rules: { type: 'json', description: 'Alert rules list' }, contactPoints: { type: 'json', description: 'Contact points list' }, + condition: { type: 'string', description: 'Alert condition refId' }, + for: { type: 'string', description: 'Duration the condition must hold before firing' }, + keepFiringFor: { + type: 'string', + description: 'Duration to keep firing after the condition stops', + }, + missingSeriesEvalsToResolve: { + type: 'number', + description: 'Missing series evaluations before resolving', + }, + isPaused: { type: 'boolean', description: 'Whether the alert rule is paused' }, + folderUID: { type: 'string', description: 'Parent folder UID' }, + ruleGroup: { type: 'string', description: 'Rule group name' }, + orgID: { type: 'number', description: 'Organization ID' }, + provenance: { type: 'string', description: 'Provisioning source' }, + noDataState: { type: 'string', description: 'State on no data' }, + execErrState: { type: 'string', description: 'State on execution error' }, + notification_settings: { type: 'json', description: 'Per-rule notification settings' }, + record: { type: 'json', description: 'Recording rule configuration' }, + updated: { type: 'string', description: 'Last update timestamp' }, // Annotation outputs annotations: { type: 'json', description: 'Annotations list' }, id: { type: 'number', description: 'Annotation ID' }, diff --git a/apps/sim/tools/grafana/create_alert_rule.ts b/apps/sim/tools/grafana/create_alert_rule.ts index a1f4fea02bc..1c78d1fba46 100644 --- a/apps/sim/tools/grafana/create_alert_rule.ts +++ b/apps/sim/tools/grafana/create_alert_rule.ts @@ -2,6 +2,7 @@ import type { GrafanaCreateAlertRuleParams, GrafanaCreateAlertRuleResponse, } from '@/tools/grafana/types' +import { alertRuleOutputFields, mapAlertRule } from '@/tools/grafana/utils' import type { ToolConfig } from '@/tools/types' export const createAlertRuleTool: ToolConfig< @@ -52,9 +53,10 @@ export const createAlertRuleTool: ToolConfig< }, condition: { type: 'string', - required: true, + required: false, visibility: 'user-or-llm', - description: 'The refId of the query or expression to use as the alert condition', + description: + 'The refId of the query or expression to use as the alert condition (required for alerting rules; omit for recording rules)', }, data: { type: 'string', @@ -78,7 +80,7 @@ export const createAlertRuleTool: ToolConfig< type: 'string', required: false, visibility: 'user-only', - description: 'State on execution error (Alerting, OK)', + description: 'State on execution error (Error, Alerting, OK)', }, annotations: { type: 'string', @@ -92,6 +94,48 @@ export const createAlertRuleTool: ToolConfig< visibility: 'user-or-llm', description: 'JSON object of labels', }, + uid: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Optional custom UID for the alert rule', + }, + isPaused: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'Whether the rule is paused on creation', + }, + keepFiringFor: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Duration to keep firing after the condition stops (e.g., 5m)', + }, + missingSeriesEvalsToResolve: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Number of missing series evaluations before resolving', + }, + notificationSettings: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'JSON object of per-rule notification settings (overrides)', + }, + record: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'JSON object configuring this as a recording rule (omit for alerting rules)', + }, + disableProvenance: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'Set X-Disable-Provenance header so the rule remains editable in the Grafana UI', + }, }, request: { @@ -105,25 +149,36 @@ export const createAlertRuleTool: ToolConfig< if (params.organizationId) { headers['X-Grafana-Org-Id'] = params.organizationId } + if (params.disableProvenance) { + headers['X-Disable-Provenance'] = 'true' + } return headers }, body: (params) => { - let dataArray: any[] = [] + let dataArray: unknown[] = [] try { dataArray = JSON.parse(params.data) } catch { throw new Error('Invalid JSON for data parameter') } - const body: Record = { + const body: Record = { + orgID: params.organizationId ? Number(params.organizationId) : 1, title: params.title, folderUID: params.folderUid, ruleGroup: params.ruleGroup, - condition: params.condition, data: dataArray, - for: params.forDuration || '5m', - noDataState: params.noDataState || 'NoData', - execErrState: params.execErrState || 'Alerting', + } + + if (params.condition) body.condition = params.condition + if (params.uid) body.uid = params.uid + if (params.forDuration) body.for = params.forDuration + if (params.noDataState) body.noDataState = params.noDataState + if (params.execErrState) body.execErrState = params.execErrState + if (params.isPaused !== undefined) body.isPaused = params.isPaused + if (params.keepFiringFor) body.keepFiringFor = params.keepFiringFor + if (params.missingSeriesEvalsToResolve !== undefined) { + body.missing_series_evals_to_resolve = params.missingSeriesEvalsToResolve } if (params.annotations) { @@ -142,53 +197,30 @@ export const createAlertRuleTool: ToolConfig< } } + if (params.notificationSettings) { + try { + body.notification_settings = JSON.parse(params.notificationSettings) + } catch { + // skip invalid notification settings JSON + } + } + + if (params.record) { + try { + body.record = JSON.parse(params.record) + } catch { + // skip invalid record JSON + } + } + return body }, }, transformResponse: async (response: Response) => { const data = await response.json() - - return { - success: true, - output: { - uid: data.uid, - title: data.title, - condition: data.condition, - data: data.data, - updated: data.updated, - noDataState: data.noDataState, - execErrState: data.execErrState, - for: data.for, - annotations: data.annotations || {}, - labels: data.labels || {}, - isPaused: data.isPaused || false, - folderUID: data.folderUID, - ruleGroup: data.ruleGroup, - orgId: data.orgId, - namespace_uid: data.namespace_uid, - namespace_id: data.namespace_id, - provenance: data.provenance || '', - }, - } + return { success: true, output: mapAlertRule(data) } }, - outputs: { - uid: { - type: 'string', - description: 'The UID of the created alert rule', - }, - title: { - type: 'string', - description: 'Alert rule title', - }, - folderUID: { - type: 'string', - description: 'Parent folder UID', - }, - ruleGroup: { - type: 'string', - description: 'Rule group name', - }, - }, + outputs: alertRuleOutputFields, } diff --git a/apps/sim/tools/grafana/create_annotation.ts b/apps/sim/tools/grafana/create_annotation.ts index dc75e3f5881..9b3c8859ed5 100644 --- a/apps/sim/tools/grafana/create_annotation.ts +++ b/apps/sim/tools/grafana/create_annotation.ts @@ -46,9 +46,10 @@ export const createAnnotationTool: ToolConfig< }, dashboardUid: { type: 'string', - required: true, + required: false, visibility: 'user-or-llm', - description: 'UID of the dashboard to add the annotation to (e.g., abc123def)', + description: + 'UID of the dashboard to add the annotation to (e.g., abc123def). Omit to create a global organization annotation.', }, panelId: { type: 'number', @@ -84,11 +85,15 @@ export const createAnnotationTool: ToolConfig< return headers }, body: (params) => { - const body: Record = { + const body: Record = { text: params.text, - time: params.time || Date.now(), } + if (params.time) body.time = params.time + if (params.timeEnd) body.timeEnd = params.timeEnd + if (params.dashboardUid) body.dashboardUID = params.dashboardUid + if (params.panelId) body.panelId = params.panelId + if (params.tags) { body.tags = params.tags .split(',') @@ -96,18 +101,6 @@ export const createAnnotationTool: ToolConfig< .filter((t) => t) } - if (params.dashboardUid) { - body.dashboardUID = params.dashboardUid - } - - if (params.panelId) { - body.panelId = params.panelId - } - - if (params.timeEnd) { - body.timeEnd = params.timeEnd - } - return body }, }, diff --git a/apps/sim/tools/grafana/create_folder.ts b/apps/sim/tools/grafana/create_folder.ts index 3231690d3b5..228a9652590 100644 --- a/apps/sim/tools/grafana/create_folder.ts +++ b/apps/sim/tools/grafana/create_folder.ts @@ -39,6 +39,12 @@ export const createFolderTool: ToolConfig { - const body: Record = { + const body: Record = { title: params.title, } - if (params.uid) { - body.uid = params.uid - } + if (params.uid) body.uid = params.uid + if (params.parentUid) body.parentUid = params.parentUid return body }, @@ -73,80 +78,80 @@ export const createFolderTool: ToolConfig = @@ -53,71 +54,8 @@ export const getAlertRuleTool: ToolConfig { const data = await response.json() - - return { - success: true, - output: { - uid: data.uid, - title: data.title, - condition: data.condition, - data: data.data, - updated: data.updated, - noDataState: data.noDataState, - execErrState: data.execErrState, - for: data.for, - annotations: data.annotations || {}, - labels: data.labels || {}, - isPaused: data.isPaused || false, - folderUID: data.folderUID, - ruleGroup: data.ruleGroup, - orgId: data.orgId, - namespace_uid: data.namespace_uid, - namespace_id: data.namespace_id, - provenance: data.provenance || '', - }, - } + return { success: true, output: mapAlertRule(data) } }, - outputs: { - uid: { - type: 'string', - description: 'Alert rule UID', - }, - title: { - type: 'string', - description: 'Alert rule title', - }, - condition: { - type: 'string', - description: 'Alert condition', - }, - data: { - type: 'json', - description: 'Alert rule query data', - }, - folderUID: { - type: 'string', - description: 'Parent folder UID', - }, - ruleGroup: { - type: 'string', - description: 'Rule group name', - }, - noDataState: { - type: 'string', - description: 'State when no data is returned', - }, - execErrState: { - type: 'string', - description: 'State on execution error', - }, - annotations: { - type: 'json', - description: 'Alert annotations', - }, - labels: { - type: 'json', - description: 'Alert labels', - }, - }, + outputs: alertRuleOutputFields, } diff --git a/apps/sim/tools/grafana/get_data_source.ts b/apps/sim/tools/grafana/get_data_source.ts index e70377a5575..1ef7bfaa1bc 100644 --- a/apps/sim/tools/grafana/get_data_source.ts +++ b/apps/sim/tools/grafana/get_data_source.ts @@ -43,12 +43,14 @@ export const getDataSourceTool: ToolConfig< request: { url: (params) => { const baseUrl = params.baseUrl.replace(/\/$/, '') - // Check if it looks like a UID (contains non-numeric characters) or ID - const isUid = /[^0-9]/.test(params.dataSourceId) - if (isUid) { - return `${baseUrl}/api/datasources/uid/${params.dataSourceId}` + const id = params.dataSourceId.trim() + // Numeric DB id route only matches purely-numeric ids up to int64 length; + // anything else is treated as a UID (Grafana UIDs are short slug strings). + const isNumericId = /^\d+$/.test(id) && id.length <= 18 + if (isNumericId) { + return `${baseUrl}/api/datasources/${id}` } - return `${baseUrl}/api/datasources/${params.dataSourceId}` + return `${baseUrl}/api/datasources/uid/${id}` }, method: 'GET', headers: (params) => { @@ -69,57 +71,54 @@ export const getDataSourceTool: ToolConfig< return { success: true, output: { - id: data.id, - uid: data.uid, - orgId: data.orgId, - name: data.name, - type: data.type, - typeName: data.typeName, - typeLogoUrl: data.typeLogoUrl, - access: data.access, - url: data.url, - user: data.user, - database: data.database, - basicAuth: data.basicAuth || false, - isDefault: data.isDefault || false, - jsonData: data.jsonData || {}, - readOnly: data.readOnly || false, + id: (data.id as number) ?? null, + uid: (data.uid as string) ?? null, + orgId: (data.orgId as number) ?? null, + name: (data.name as string) ?? null, + type: (data.type as string) ?? null, + typeLogoUrl: (data.typeLogoUrl as string) ?? null, + access: (data.access as string) ?? null, + url: (data.url as string) ?? null, + user: (data.user as string) ?? null, + database: (data.database as string) ?? null, + basicAuth: (data.basicAuth as boolean) ?? false, + basicAuthUser: (data.basicAuthUser as string) ?? null, + withCredentials: (data.withCredentials as boolean) ?? null, + isDefault: (data.isDefault as boolean) ?? false, + jsonData: (data.jsonData as Record) ?? {}, + secureJsonFields: (data.secureJsonFields as Record) ?? {}, + version: (data.version as number) ?? null, + readOnly: (data.readOnly as boolean) ?? false, }, } }, outputs: { - id: { - type: 'number', - description: 'Data source ID', - }, - uid: { - type: 'string', - description: 'Data source UID', - }, - name: { - type: 'string', - description: 'Data source name', - }, - type: { - type: 'string', - description: 'Data source type', - }, - url: { - type: 'string', - description: 'Data source connection URL', - }, - database: { - type: 'string', - description: 'Database name (if applicable)', - }, - isDefault: { + id: { type: 'number', description: 'Data source ID' }, + uid: { type: 'string', description: 'Data source UID' }, + orgId: { type: 'number', description: 'Organization ID' }, + name: { type: 'string', description: 'Data source name' }, + type: { type: 'string', description: 'Data source type' }, + typeLogoUrl: { type: 'string', description: 'Logo URL for the data source type' }, + access: { type: 'string', description: 'Access mode (proxy or direct)' }, + url: { type: 'string', description: 'Data source connection URL' }, + user: { type: 'string', description: 'Username used to connect' }, + database: { type: 'string', description: 'Database name (if applicable)' }, + basicAuth: { type: 'boolean', description: 'Whether basic auth is enabled' }, + basicAuthUser: { type: 'string', description: 'Basic auth username', optional: true }, + withCredentials: { type: 'boolean', - description: 'Whether this is the default data source', + description: 'Whether to send credentials with cross-origin requests', + optional: true, }, - jsonData: { - type: 'json', - description: 'Additional data source configuration', + isDefault: { type: 'boolean', description: 'Whether this is the default data source' }, + jsonData: { type: 'json', description: 'Additional data source configuration' }, + secureJsonFields: { + type: 'object', + description: 'Map of secure fields that are set (values are not returned)', + optional: true, }, + version: { type: 'number', description: 'Data source version', optional: true }, + readOnly: { type: 'boolean', description: 'Whether the data source is read-only' }, }, } diff --git a/apps/sim/tools/grafana/list_alert_rules.ts b/apps/sim/tools/grafana/list_alert_rules.ts index 8f85c56ab28..f68c6dd6dc9 100644 --- a/apps/sim/tools/grafana/list_alert_rules.ts +++ b/apps/sim/tools/grafana/list_alert_rules.ts @@ -2,6 +2,7 @@ import type { GrafanaListAlertRulesParams, GrafanaListAlertRulesResponse, } from '@/tools/grafana/types' +import { alertRuleOutputFields, mapAlertRule } from '@/tools/grafana/utils' import type { ToolConfig } from '@/tools/types' export const listAlertRulesTool: ToolConfig< @@ -56,25 +57,7 @@ export const listAlertRulesTool: ToolConfig< success: true, output: { rules: Array.isArray(data) - ? data.map((rule: any) => ({ - uid: rule.uid, - title: rule.title, - condition: rule.condition, - data: rule.data, - updated: rule.updated, - noDataState: rule.noDataState, - execErrState: rule.execErrState, - for: rule.for, - annotations: rule.annotations || {}, - labels: rule.labels || {}, - isPaused: rule.isPaused || false, - folderUID: rule.folderUID, - ruleGroup: rule.ruleGroup, - orgId: rule.orgId, - namespace_uid: rule.namespace_uid, - namespace_id: rule.namespace_id, - provenance: rule.provenance || '', - })) + ? data.map((rule: Record) => mapAlertRule(rule)) : [], }, } @@ -86,15 +69,7 @@ export const listAlertRulesTool: ToolConfig< description: 'List of alert rules', items: { type: 'object', - properties: { - uid: { type: 'string', description: 'Alert rule UID' }, - title: { type: 'string', description: 'Alert rule title' }, - condition: { type: 'string', description: 'Alert condition' }, - folderUID: { type: 'string', description: 'Parent folder UID' }, - ruleGroup: { type: 'string', description: 'Rule group name' }, - noDataState: { type: 'string', description: 'State when no data is returned' }, - execErrState: { type: 'string', description: 'State on execution error' }, - }, + properties: alertRuleOutputFields, }, }, }, diff --git a/apps/sim/tools/grafana/list_annotations.ts b/apps/sim/tools/grafana/list_annotations.ts index b267c3f236d..3c3c9af3cf0 100644 --- a/apps/sim/tools/grafana/list_annotations.ts +++ b/apps/sim/tools/grafana/list_annotations.ts @@ -46,9 +46,16 @@ export const listAnnotationsTool: ToolConfig< }, dashboardUid: { type: 'string', - required: true, + required: false, visibility: 'user-or-llm', - description: 'Dashboard UID to query annotations from (e.g., abc123def)', + description: + 'Dashboard UID to query annotations from (e.g., abc123def). Omit to query annotations across the organization.', + }, + dashboardId: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Legacy numeric dashboard ID filter (prefer dashboardUid)', }, panelId: { type: 'number', @@ -56,6 +63,18 @@ export const listAnnotationsTool: ToolConfig< visibility: 'user-or-llm', description: 'Filter by panel ID (e.g., 1, 2)', }, + alertId: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Filter by alert ID', + }, + userId: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Filter by ID of the user who created the annotation', + }, tags: { type: 'string', required: false, @@ -84,7 +103,10 @@ export const listAnnotationsTool: ToolConfig< if (params.from) searchParams.set('from', String(params.from)) if (params.to) searchParams.set('to', String(params.to)) if (params.dashboardUid) searchParams.set('dashboardUID', params.dashboardUid) + if (params.dashboardId) searchParams.set('dashboardId', String(params.dashboardId)) if (params.panelId) searchParams.set('panelId', String(params.panelId)) + if (params.alertId) searchParams.set('alertId', String(params.alertId)) + if (params.userId) searchParams.set('userId', String(params.userId)) if (params.tags) { params.tags.split(',').forEach((t) => searchParams.append('tags', t.trim())) } @@ -109,27 +131,27 @@ export const listAnnotationsTool: ToolConfig< transformResponse: async (response: Response) => { const data = await response.json() - - // Handle potential nested array structure const rawAnnotations = Array.isArray(data) ? data.flat() : [] return { success: true, output: { - annotations: rawAnnotations.map((a: any) => ({ - id: a.id, - dashboardId: a.dashboardId, - dashboardUID: a.dashboardUID, - created: a.created, - updated: a.updated, - time: a.time, - timeEnd: a.timeEnd, - text: a.text, - tags: a.tags || [], - login: a.login, - email: a.email, - avatarUrl: a.avatarUrl, - data: a.data || {}, + annotations: rawAnnotations.map((a: Record) => ({ + id: (a.id as number) ?? null, + alertId: (a.alertId as number) ?? null, + dashboardId: (a.dashboardId as number) ?? null, + dashboardUID: (a.dashboardUID as string) ?? null, + panelId: (a.panelId as number) ?? null, + userId: (a.userId as number) ?? null, + userName: (a.userName as string) ?? null, + newState: (a.newState as string) ?? null, + prevState: (a.prevState as string) ?? null, + time: (a.time as number) ?? null, + timeEnd: (a.timeEnd as number) ?? null, + text: (a.text as string) ?? null, + metric: (a.metric as string) ?? null, + tags: (a.tags as string[]) ?? [], + data: (a.data as Record) ?? {}, })), }, } @@ -143,21 +165,36 @@ export const listAnnotationsTool: ToolConfig< type: 'object', properties: { id: { type: 'number', description: 'Annotation ID' }, - dashboardId: { type: 'number', description: 'Dashboard ID' }, - dashboardUID: { type: 'string', description: 'Dashboard UID' }, - created: { type: 'number', description: 'Creation timestamp in epoch ms' }, - updated: { type: 'number', description: 'Last update timestamp in epoch ms' }, + alertId: { type: 'number', description: 'Associated alert ID (0 if not alert-driven)' }, + dashboardId: { type: 'number', description: 'Dashboard ID', optional: true }, + dashboardUID: { type: 'string', description: 'Dashboard UID', optional: true }, + panelId: { type: 'number', description: 'Panel ID within the dashboard', optional: true }, + userId: { type: 'number', description: 'ID of the user who created the annotation' }, + userName: { + type: 'string', + description: 'Username of the user who created the annotation', + optional: true, + }, + newState: { + type: 'string', + description: 'New alert state (alert annotations only)', + optional: true, + }, + prevState: { + type: 'string', + description: 'Previous alert state (alert annotations only)', + optional: true, + }, time: { type: 'number', description: 'Start time in epoch ms' }, - timeEnd: { type: 'number', description: 'End time in epoch ms' }, + timeEnd: { type: 'number', description: 'End time in epoch ms', optional: true }, text: { type: 'string', description: 'Annotation text' }, - tags: { type: 'array', items: { type: 'string' }, description: 'Annotation tags' }, - login: { type: 'string', description: 'Login of the user who created the annotation' }, - email: { type: 'string', description: 'Email of the user who created the annotation' }, - avatarUrl: { type: 'string', description: 'Avatar URL of the user' }, - data: { - type: 'json', - description: 'Additional annotation data object from Grafana', + metric: { + type: 'string', + description: 'Metric associated with the annotation', + optional: true, }, + tags: { type: 'array', items: { type: 'string' }, description: 'Annotation tags' }, + data: { type: 'json', description: 'Additional annotation data object from Grafana' }, }, }, }, diff --git a/apps/sim/tools/grafana/list_contact_points.ts b/apps/sim/tools/grafana/list_contact_points.ts index 40e339e8ccc..cf044f21c68 100644 --- a/apps/sim/tools/grafana/list_contact_points.ts +++ b/apps/sim/tools/grafana/list_contact_points.ts @@ -32,10 +32,22 @@ export const listContactPointsTool: ToolConfig< visibility: 'user-or-llm', description: 'Organization ID for multi-org Grafana instances (e.g., 1, 2)', }, + name: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter contact points by exact name match', + }, }, request: { - url: (params) => `${params.baseUrl.replace(/\/$/, '')}/api/v1/provisioning/contact-points`, + url: (params) => { + const baseUrl = params.baseUrl.replace(/\/$/, '') + const searchParams = new URLSearchParams() + if (params.name) searchParams.set('name', params.name) + const queryString = searchParams.toString() + return `${baseUrl}/api/v1/provisioning/contact-points${queryString ? `?${queryString}` : ''}` + }, method: 'GET', headers: (params) => { const headers: Record = { @@ -56,13 +68,13 @@ export const listContactPointsTool: ToolConfig< success: true, output: { contactPoints: Array.isArray(data) - ? data.map((cp: any) => ({ - uid: cp.uid, - name: cp.name, - type: cp.type, - settings: cp.settings || {}, - disableResolveMessage: cp.disableResolveMessage || false, - provenance: cp.provenance || '', + ? data.map((cp: Record) => ({ + uid: (cp.uid as string) ?? null, + name: (cp.name as string) ?? null, + type: (cp.type as string) ?? null, + settings: (cp.settings as Record) ?? {}, + disableResolveMessage: (cp.disableResolveMessage as boolean) ?? false, + provenance: (cp.provenance as string) ?? '', })) : [], }, @@ -80,6 +92,14 @@ export const listContactPointsTool: ToolConfig< name: { type: 'string', description: 'Contact point name' }, type: { type: 'string', description: 'Notification type (email, slack, etc.)' }, settings: { type: 'object', description: 'Type-specific settings' }, + disableResolveMessage: { + type: 'boolean', + description: 'Whether resolve messages are disabled', + }, + provenance: { + type: 'string', + description: 'Provisioning source (empty if API-managed)', + }, }, }, }, diff --git a/apps/sim/tools/grafana/list_dashboards.ts b/apps/sim/tools/grafana/list_dashboards.ts index 855f008415a..39299199a54 100644 --- a/apps/sim/tools/grafana/list_dashboards.ts +++ b/apps/sim/tools/grafana/list_dashboards.ts @@ -44,11 +44,17 @@ export const listDashboardsTool: ToolConfig< visibility: 'user-or-llm', description: 'Filter by tag (comma-separated for multiple tags)', }, - folderIds: { + folderUIDs: { type: 'string', required: false, visibility: 'user-or-llm', - description: 'Filter by folder IDs (comma-separated, e.g., 1,2,3)', + description: 'Filter by folder UIDs (comma-separated, e.g., abc123,def456)', + }, + dashboardUIDs: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by dashboard UIDs (comma-separated, e.g., abc123,def456)', }, starred: { type: 'boolean', @@ -60,7 +66,13 @@ export const listDashboardsTool: ToolConfig< type: 'number', required: false, visibility: 'user-only', - description: 'Maximum number of dashboards to return', + description: 'Maximum number of dashboards to return (default 1000)', + }, + page: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Page number for pagination (1-based)', }, }, @@ -74,11 +86,17 @@ export const listDashboardsTool: ToolConfig< if (params.tag) { params.tag.split(',').forEach((t) => searchParams.append('tag', t.trim())) } - if (params.folderIds) { - params.folderIds.split(',').forEach((id) => searchParams.append('folderIds', id.trim())) + if (params.folderUIDs) { + params.folderUIDs.split(',').forEach((uid) => searchParams.append('folderUIDs', uid.trim())) + } + if (params.dashboardUIDs) { + params.dashboardUIDs + .split(',') + .forEach((uid) => searchParams.append('dashboardUIDs', uid.trim())) } if (params.starred) searchParams.set('starred', 'true') if (params.limit) searchParams.set('limit', String(params.limit)) + if (params.page) searchParams.set('page', String(params.page)) return `${baseUrl}/api/search?${searchParams.toString()}` }, @@ -102,21 +120,19 @@ export const listDashboardsTool: ToolConfig< success: true, output: { dashboards: Array.isArray(data) - ? data.map((d: any) => ({ - id: d.id, - uid: d.uid, - title: d.title, - uri: d.uri, - url: d.url, - slug: d.slug, - type: d.type, - tags: d.tags || [], - isStarred: d.isStarred || false, - folderId: d.folderId, - folderUid: d.folderUid, - folderTitle: d.folderTitle, - folderUrl: d.folderUrl, - sortMeta: d.sortMeta, + ? data.map((d: Record) => ({ + id: (d.id as number) ?? null, + uid: (d.uid as string) ?? null, + title: (d.title as string) ?? null, + uri: (d.uri as string) ?? null, + url: (d.url as string) ?? null, + type: (d.type as string) ?? null, + tags: (d.tags as string[]) ?? [], + isStarred: (d.isStarred as boolean) ?? false, + folderId: (d.folderId as number) ?? null, + folderUid: (d.folderUid as string) ?? null, + folderTitle: (d.folderTitle as string) ?? null, + folderUrl: (d.folderUrl as string) ?? null, })) : [], }, diff --git a/apps/sim/tools/grafana/list_data_sources.ts b/apps/sim/tools/grafana/list_data_sources.ts index 55bd0ce4208..2c826248fce 100644 --- a/apps/sim/tools/grafana/list_data_sources.ts +++ b/apps/sim/tools/grafana/list_data_sources.ts @@ -56,22 +56,25 @@ export const listDataSourcesTool: ToolConfig< success: true, output: { dataSources: Array.isArray(data) - ? data.map((ds: any) => ({ - id: ds.id, - uid: ds.uid, - orgId: ds.orgId, - name: ds.name, - type: ds.type, - typeName: ds.typeName, - typeLogoUrl: ds.typeLogoUrl, - access: ds.access, - url: ds.url, - user: ds.user, - database: ds.database, - basicAuth: ds.basicAuth || false, - isDefault: ds.isDefault || false, - jsonData: ds.jsonData || {}, - readOnly: ds.readOnly || false, + ? data.map((ds: Record) => ({ + id: (ds.id as number) ?? null, + uid: (ds.uid as string) ?? null, + orgId: (ds.orgId as number) ?? null, + name: (ds.name as string) ?? null, + type: (ds.type as string) ?? null, + typeLogoUrl: (ds.typeLogoUrl as string) ?? null, + access: (ds.access as string) ?? null, + url: (ds.url as string) ?? null, + user: (ds.user as string) ?? null, + database: (ds.database as string) ?? null, + basicAuth: (ds.basicAuth as boolean) ?? false, + basicAuthUser: (ds.basicAuthUser as string) ?? null, + withCredentials: (ds.withCredentials as boolean) ?? null, + isDefault: (ds.isDefault as boolean) ?? false, + jsonData: (ds.jsonData as Record) ?? {}, + secureJsonFields: (ds.secureJsonFields as Record) ?? {}, + version: (ds.version as number) ?? null, + readOnly: (ds.readOnly as boolean) ?? false, })) : [], }, @@ -87,10 +90,34 @@ export const listDataSourcesTool: ToolConfig< properties: { id: { type: 'number', description: 'Data source ID' }, uid: { type: 'string', description: 'Data source UID' }, + orgId: { type: 'number', description: 'Organization ID' }, name: { type: 'string', description: 'Data source name' }, type: { type: 'string', description: 'Data source type (prometheus, mysql, etc.)' }, + typeLogoUrl: { type: 'string', description: 'Logo URL for the data source type' }, + access: { type: 'string', description: 'Access mode (proxy or direct)' }, url: { type: 'string', description: 'Data source URL' }, + user: { type: 'string', description: 'Username used to connect' }, + database: { type: 'string', description: 'Database name (if applicable)' }, + basicAuth: { type: 'boolean', description: 'Whether basic auth is enabled' }, + basicAuthUser: { + type: 'string', + description: 'Basic auth username', + optional: true, + }, + withCredentials: { + type: 'boolean', + description: 'Whether to send credentials with cross-origin requests', + optional: true, + }, isDefault: { type: 'boolean', description: 'Whether this is the default data source' }, + jsonData: { type: 'object', description: 'Type-specific JSON configuration' }, + secureJsonFields: { + type: 'object', + description: 'Map of secure fields that are set (values are not returned)', + optional: true, + }, + version: { type: 'number', description: 'Data source version', optional: true }, + readOnly: { type: 'boolean', description: 'Whether the data source is read-only' }, }, }, }, diff --git a/apps/sim/tools/grafana/list_folders.ts b/apps/sim/tools/grafana/list_folders.ts index 03aa569d672..85b1afc5500 100644 --- a/apps/sim/tools/grafana/list_folders.ts +++ b/apps/sim/tools/grafana/list_folders.ts @@ -38,6 +38,12 @@ export const listFoldersTool: ToolConfig ({ - id: f.id, - uid: f.uid, - title: f.title, - hasAcl: f.hasAcl || false, - canSave: f.canSave || false, - canEdit: f.canEdit || false, - canAdmin: f.canAdmin || false, - canDelete: f.canDelete || false, - createdBy: f.createdBy || '', - created: f.created || '', - updatedBy: f.updatedBy || '', - updated: f.updated || '', - version: f.version || 0, + ? data.map((f: Record) => ({ + id: (f.id as number) ?? null, + uid: (f.uid as string) ?? null, + title: (f.title as string) ?? null, + url: (f.url as string) ?? null, + parentUid: (f.parentUid as string) ?? null, + parents: (f.parents as { uid: string; title: string; url: string }[]) ?? [], + hasAcl: (f.hasAcl as boolean) ?? null, + canSave: (f.canSave as boolean) ?? null, + canEdit: (f.canEdit as boolean) ?? null, + canAdmin: (f.canAdmin as boolean) ?? null, + createdBy: (f.createdBy as string) ?? null, + created: (f.created as string) ?? null, + updatedBy: (f.updatedBy as string) ?? null, + updated: (f.updated as string) ?? null, + version: (f.version as number) ?? null, })) : [], }, @@ -101,19 +110,58 @@ export const listFoldersTool: ToolConfig[] + templating: Record + annotations: Record time: { from: string to: string @@ -88,26 +88,26 @@ export interface GrafanaGetDashboardResponse extends ToolResponse { export interface GrafanaListDashboardsParams extends GrafanaBaseParams { query?: string tag?: string - folderIds?: string + folderUIDs?: string + dashboardUIDs?: string starred?: boolean limit?: number + page?: number } interface GrafanaDashboardSearchResult { - id: number - uid: string - title: string - uri: string - url: string - slug: string - type: string + id: number | null + uid: string | null + title: string | null + uri: string | null + url: string | null + type: string | null tags: string[] isStarred: boolean - folderId: number - folderUid: string - folderTitle: string - folderUrl: string - sortMeta: number + folderId: number | null + folderUid: string | null + folderTitle: string | null + folderUrl: string | null } export interface GrafanaListDashboardsResponse extends ToolResponse { @@ -177,23 +177,26 @@ export interface GrafanaDeleteDashboardResponse extends ToolResponse { export interface GrafanaListAlertRulesParams extends GrafanaBaseParams {} interface GrafanaAlertRule { - uid: string - title: string - condition: string - data: any[] - updated: string - noDataState: string - execErrState: string - for: string + id: number | null + uid: string | null + title: string | null + condition: string | null + data: unknown[] + updated: string | null + noDataState: string | null + execErrState: string | null + for: string | null + keepFiringFor: string | null + missingSeriesEvalsToResolve: number | null annotations: Record labels: Record isPaused: boolean - folderUID: string - ruleGroup: string - orgId: number - namespace_uid: string - namespace_id: number + folderUID: string | null + ruleGroup: string | null + orgID: number | null provenance: string + notification_settings: Record | null + record: Record | null } export interface GrafanaListAlertRulesResponse extends ToolResponse { @@ -214,13 +217,20 @@ export interface GrafanaCreateAlertRuleParams extends GrafanaBaseParams { title: string folderUid: string ruleGroup: string - condition: string + condition?: string data: string // JSON string of data array forDuration?: string noDataState?: string execErrState?: string annotations?: string // JSON string labels?: string // JSON string + uid?: string + isPaused?: boolean + keepFiringFor?: string + missingSeriesEvalsToResolve?: number + notificationSettings?: string // JSON string + record?: string // JSON string + disableProvenance?: boolean } export interface GrafanaCreateAlertRuleResponse extends ToolResponse { @@ -239,6 +249,12 @@ export interface GrafanaUpdateAlertRuleParams extends GrafanaBaseParams { execErrState?: string annotations?: string // JSON string labels?: string // JSON string + isPaused?: boolean + keepFiringFor?: string + missingSeriesEvalsToResolve?: number + notificationSettings?: string // JSON string + record?: string // JSON string + disableProvenance?: boolean } interface GrafanaUpdateAlertRuleResponse extends ToolResponse { @@ -266,19 +282,21 @@ export interface GrafanaCreateAnnotationParams extends GrafanaBaseParams { } interface GrafanaAnnotation { - id: number - dashboardId: number - dashboardUID: string - created: number - updated: number - time: number - timeEnd: number - text: string + id: number | null + alertId: number | null + dashboardId: number | null + dashboardUID: string | null + panelId: number | null + userId: number | null + userName: string | null + newState: string | null + prevState: string | null + time: number | null + timeEnd: number | null + text: string | null + metric: string | null tags: string[] - login: string - email: string - avatarUrl: string - data: any + data: Record } export interface GrafanaCreateAnnotationResponse extends ToolResponse { @@ -291,8 +309,11 @@ export interface GrafanaCreateAnnotationResponse extends ToolResponse { export interface GrafanaListAnnotationsParams extends GrafanaBaseParams { from?: number to?: number + dashboardId?: number dashboardUid?: string panelId?: number + alertId?: number + userId?: number tags?: string // comma-separated type?: string limit?: number @@ -306,7 +327,7 @@ export interface GrafanaListAnnotationsResponse extends ToolResponse { export interface GrafanaUpdateAnnotationParams extends GrafanaBaseParams { annotationId: number - text: string + text?: string tags?: string // comma-separated time?: number timeEnd?: number @@ -338,15 +359,18 @@ interface GrafanaDataSource { orgId: number name: string type: string - typeName: string typeLogoUrl: string access: string url: string user: string database: string basicAuth: boolean + basicAuthUser?: string + withCredentials?: boolean isDefault: boolean - jsonData: any + jsonData: Record + secureJsonFields?: Record + version?: number readOnly: boolean } @@ -368,22 +392,31 @@ export interface GrafanaGetDataSourceResponse extends ToolResponse { export interface GrafanaListFoldersParams extends GrafanaBaseParams { limit?: number page?: number + parentUid?: string +} + +interface GrafanaFolderParent { + uid: string + title: string + url: string } interface GrafanaFolder { id: number uid: string title: string - hasAcl: boolean - canSave: boolean - canEdit: boolean - canAdmin: boolean - canDelete: boolean - createdBy: string - created: string - updatedBy: string - updated: string - version: number + url?: string + hasAcl?: boolean + canSave?: boolean + canEdit?: boolean + canAdmin?: boolean + createdBy?: string + created?: string + updatedBy?: string + updated?: string + version?: number + parentUid?: string | null + parents?: GrafanaFolderParent[] } export interface GrafanaListFoldersResponse extends ToolResponse { @@ -395,6 +428,7 @@ export interface GrafanaListFoldersResponse extends ToolResponse { export interface GrafanaCreateFolderParams extends GrafanaBaseParams { title: string uid?: string + parentUid?: string } export interface GrafanaCreateFolderResponse extends ToolResponse { @@ -402,13 +436,15 @@ export interface GrafanaCreateFolderResponse extends ToolResponse { } // Contact Points types -export interface GrafanaListContactPointsParams extends GrafanaBaseParams {} +export interface GrafanaListContactPointsParams extends GrafanaBaseParams { + name?: string +} interface GrafanaContactPoint { uid: string name: string type: string - settings: Record + settings: Record disableResolveMessage: boolean provenance: string } diff --git a/apps/sim/tools/grafana/update_alert_rule.ts b/apps/sim/tools/grafana/update_alert_rule.ts index 386a9b0e825..3c0f4f024a1 100644 --- a/apps/sim/tools/grafana/update_alert_rule.ts +++ b/apps/sim/tools/grafana/update_alert_rule.ts @@ -1,4 +1,5 @@ import type { GrafanaUpdateAlertRuleParams } from '@/tools/grafana/types' +import { alertRuleOutputFields, mapAlertRule } from '@/tools/grafana/utils' import type { ToolConfig, ToolResponse } from '@/tools/types' // Using ToolResponse for intermediate state since this tool fetches existing data first @@ -93,6 +94,42 @@ export const updateAlertRuleTool: ToolConfig = { + const updatedRule: Record = { ...existingRule, } @@ -148,6 +185,27 @@ export const updateAlertRuleTool: ToolConfig { - const body: Record = { - text: params.text, - } + const body: Record = {} + + if (params.text !== undefined) body.text = params.text + if (params.time) body.time = params.time + if (params.timeEnd) body.timeEnd = params.timeEnd if (params.tags) { body.tags = params.tags @@ -89,14 +91,6 @@ export const updateAnnotationTool: ToolConfig< .filter((t) => t) } - if (params.time) { - body.time = params.time - } - - if (params.timeEnd) { - body.timeEnd = params.timeEnd - } - return body }, }, diff --git a/apps/sim/tools/grafana/update_dashboard.ts b/apps/sim/tools/grafana/update_dashboard.ts index 2913878012f..23449f36830 100644 --- a/apps/sim/tools/grafana/update_dashboard.ts +++ b/apps/sim/tools/grafana/update_dashboard.ts @@ -74,7 +74,8 @@ export const updateDashboardTool: ToolConfig = { dashboard: updatedDashboard, - overwrite: params.overwrite !== false, + overwrite: params.overwrite === true, } // Use existing folder if not specified diff --git a/apps/sim/tools/grafana/utils.ts b/apps/sim/tools/grafana/utils.ts new file mode 100644 index 00000000000..b168658a2a9 --- /dev/null +++ b/apps/sim/tools/grafana/utils.ts @@ -0,0 +1,75 @@ +import type { OutputProperty } from '@/tools/types' + +/** + * Map a raw Grafana ProvisionedAlertRule JSON object to the canonical output shape + * shared across list/get/create/update alert rule tools. + */ +export function mapAlertRule(rule: Record) { + return { + id: (rule.id as number) ?? null, + uid: (rule.uid as string) ?? null, + title: (rule.title as string) ?? null, + condition: (rule.condition as string) ?? null, + data: (rule.data as unknown[]) ?? [], + updated: (rule.updated as string) ?? null, + noDataState: (rule.noDataState as string) ?? null, + execErrState: (rule.execErrState as string) ?? null, + for: (rule.for as string) ?? null, + keepFiringFor: (rule.keepFiringFor as string) ?? (rule.keep_firing_for as string) ?? null, + missingSeriesEvalsToResolve: + (rule.missing_series_evals_to_resolve as number) ?? + (rule.missingSeriesEvalsToResolve as number) ?? + null, + annotations: (rule.annotations as Record) ?? {}, + labels: (rule.labels as Record) ?? {}, + isPaused: (rule.isPaused as boolean) ?? false, + folderUID: (rule.folderUID as string) ?? null, + ruleGroup: (rule.ruleGroup as string) ?? null, + orgID: (rule.orgID as number) ?? (rule.orgId as number) ?? null, + provenance: (rule.provenance as string) ?? '', + notification_settings: (rule.notification_settings as Record) ?? null, + record: (rule.record as Record) ?? null, + } +} + +/** + * Canonical output schema fields shared across alert rule tools. + */ +export const alertRuleOutputFields: Record = { + id: { type: 'number', description: 'Alert rule numeric ID', optional: true }, + uid: { type: 'string', description: 'Alert rule UID' }, + title: { type: 'string', description: 'Alert rule title' }, + condition: { type: 'string', description: 'RefId of the query used as the alert condition' }, + data: { type: 'json', description: 'Alert rule query/expression data array' }, + updated: { type: 'string', description: 'Last update timestamp', optional: true }, + noDataState: { type: 'string', description: 'State when no data is returned' }, + execErrState: { type: 'string', description: 'State on execution error' }, + for: { type: 'string', description: 'Duration the condition must hold before firing' }, + keepFiringFor: { + type: 'string', + description: 'Duration to keep firing after condition stops', + optional: true, + }, + missingSeriesEvalsToResolve: { + type: 'number', + description: 'Number of missing series evaluations before resolving', + optional: true, + }, + annotations: { type: 'json', description: 'Alert annotations' }, + labels: { type: 'json', description: 'Alert labels' }, + isPaused: { type: 'boolean', description: 'Whether the rule is paused' }, + folderUID: { type: 'string', description: 'Parent folder UID' }, + ruleGroup: { type: 'string', description: 'Rule group name' }, + orgID: { type: 'number', description: 'Organization ID' }, + provenance: { type: 'string', description: 'Provisioning source (empty if API-managed)' }, + notification_settings: { + type: 'json', + description: 'Per-rule notification settings (overrides)', + optional: true, + }, + record: { + type: 'json', + description: 'Recording rule configuration (recording rules only)', + optional: true, + }, +} From de3caebd31b29c142a2421bf116411e60259a072 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 12 May 2026 17:19:15 -0700 Subject: [PATCH 2/5] fix(grafana): correct wire-format casing for provisioned alert rule fields MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Grafana's ProvisionedAlertRule schema (verified against upstream Go source and swagger spec) uses keep_firing_for (snake_case) and missingSeriesEvalsToResolve (camelCase) — the opposite of what prior audit rounds assumed. POST/PUT bodies now send the correct field names; mapAlertRule reads the correct primary names with the old casings kept as fallbacks. Co-Authored-By: Claude Opus 4.7 --- apps/sim/tools/grafana/create_alert_rule.ts | 4 ++-- apps/sim/tools/grafana/update_alert_rule.ts | 4 ++-- apps/sim/tools/grafana/utils.ts | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/sim/tools/grafana/create_alert_rule.ts b/apps/sim/tools/grafana/create_alert_rule.ts index 1c78d1fba46..3fff95fb9b5 100644 --- a/apps/sim/tools/grafana/create_alert_rule.ts +++ b/apps/sim/tools/grafana/create_alert_rule.ts @@ -176,9 +176,9 @@ export const createAlertRuleTool: ToolConfig< if (params.noDataState) body.noDataState = params.noDataState if (params.execErrState) body.execErrState = params.execErrState if (params.isPaused !== undefined) body.isPaused = params.isPaused - if (params.keepFiringFor) body.keepFiringFor = params.keepFiringFor + if (params.keepFiringFor) body.keep_firing_for = params.keepFiringFor if (params.missingSeriesEvalsToResolve !== undefined) { - body.missing_series_evals_to_resolve = params.missingSeriesEvalsToResolve + body.missingSeriesEvalsToResolve = params.missingSeriesEvalsToResolve } if (params.annotations) { diff --git a/apps/sim/tools/grafana/update_alert_rule.ts b/apps/sim/tools/grafana/update_alert_rule.ts index 3c0f4f024a1..0ba038acf48 100644 --- a/apps/sim/tools/grafana/update_alert_rule.ts +++ b/apps/sim/tools/grafana/update_alert_rule.ts @@ -186,9 +186,9 @@ export const updateAlertRuleTool: ToolConfig) { noDataState: (rule.noDataState as string) ?? null, execErrState: (rule.execErrState as string) ?? null, for: (rule.for as string) ?? null, - keepFiringFor: (rule.keepFiringFor as string) ?? (rule.keep_firing_for as string) ?? null, + keepFiringFor: (rule.keep_firing_for as string) ?? (rule.keepFiringFor as string) ?? null, missingSeriesEvalsToResolve: - (rule.missing_series_evals_to_resolve as number) ?? (rule.missingSeriesEvalsToResolve as number) ?? + (rule.missing_series_evals_to_resolve as number) ?? null, annotations: (rule.annotations as Record) ?? {}, labels: (rule.labels as Record) ?? {}, From da17194b76ef92e4ee2ee02ca52a64010ff5c6dd Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 12 May 2026 17:20:25 -0700 Subject: [PATCH 3/5] fix(grafana): address PR review feedback - Drop hardcoded orgID: 1 fallback; only send orgID when organizationId is provided, so token-scoped org context drives rule placement. - Surface invalid JSON for notificationSettings/record on alert rule create/update instead of silently dropping the input. - Fix execErrState description in update_alert_rule to include Error. Co-Authored-By: Claude Opus 4.7 --- apps/sim/tools/grafana/create_alert_rule.ts | 6 +++--- apps/sim/tools/grafana/update_alert_rule.ts | 14 +++++++++++--- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/apps/sim/tools/grafana/create_alert_rule.ts b/apps/sim/tools/grafana/create_alert_rule.ts index 3fff95fb9b5..0343f5e1184 100644 --- a/apps/sim/tools/grafana/create_alert_rule.ts +++ b/apps/sim/tools/grafana/create_alert_rule.ts @@ -163,12 +163,12 @@ export const createAlertRuleTool: ToolConfig< } const body: Record = { - orgID: params.organizationId ? Number(params.organizationId) : 1, title: params.title, folderUID: params.folderUid, ruleGroup: params.ruleGroup, data: dataArray, } + if (params.organizationId) body.orgID = Number(params.organizationId) if (params.condition) body.condition = params.condition if (params.uid) body.uid = params.uid @@ -201,7 +201,7 @@ export const createAlertRuleTool: ToolConfig< try { body.notification_settings = JSON.parse(params.notificationSettings) } catch { - // skip invalid notification settings JSON + throw new Error('Invalid JSON for notificationSettings parameter') } } @@ -209,7 +209,7 @@ export const createAlertRuleTool: ToolConfig< try { body.record = JSON.parse(params.record) } catch { - // skip invalid record JSON + throw new Error('Invalid JSON for record parameter') } } diff --git a/apps/sim/tools/grafana/update_alert_rule.ts b/apps/sim/tools/grafana/update_alert_rule.ts index 0ba038acf48..06b0b2a75e6 100644 --- a/apps/sim/tools/grafana/update_alert_rule.ts +++ b/apps/sim/tools/grafana/update_alert_rule.ts @@ -80,7 +80,7 @@ export const updateAlertRuleTool: ToolConfig Date: Tue, 12 May 2026 17:27:52 -0700 Subject: [PATCH 4/5] fix(grafana): surface invalid JSON for annotations/labels/data on alert rules Match the behavior of other JSON params (data, notificationSettings, record): return a descriptive error instead of silently falling back to {} (create) or keeping the existing value (update). Co-Authored-By: Claude Opus 4.7 --- apps/sim/tools/grafana/create_alert_rule.ts | 4 ++-- apps/sim/tools/grafana/update_alert_rule.ts | 18 +++++++++++++++--- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/apps/sim/tools/grafana/create_alert_rule.ts b/apps/sim/tools/grafana/create_alert_rule.ts index 0343f5e1184..27597126317 100644 --- a/apps/sim/tools/grafana/create_alert_rule.ts +++ b/apps/sim/tools/grafana/create_alert_rule.ts @@ -185,7 +185,7 @@ export const createAlertRuleTool: ToolConfig< try { body.annotations = JSON.parse(params.annotations) } catch { - body.annotations = {} + throw new Error('Invalid JSON for annotations parameter') } } @@ -193,7 +193,7 @@ export const createAlertRuleTool: ToolConfig< try { body.labels = JSON.parse(params.labels) } catch { - body.labels = {} + throw new Error('Invalid JSON for labels parameter') } } diff --git a/apps/sim/tools/grafana/update_alert_rule.ts b/apps/sim/tools/grafana/update_alert_rule.ts index 06b0b2a75e6..1fcff72a880 100644 --- a/apps/sim/tools/grafana/update_alert_rule.ts +++ b/apps/sim/tools/grafana/update_alert_rule.ts @@ -219,7 +219,11 @@ export const updateAlertRuleTool: ToolConfig Date: Tue, 12 May 2026 19:41:38 -0700 Subject: [PATCH 5/5] docs(grafana): expose alert-rule output fields in generated docs Move ALERT_RULE_OUTPUT_FIELDS from utils.ts to types.ts and rename to SCREAMING_SNAKE_CASE so scripts/generate-docs.ts (which only resolves const references from types.ts matching [A-Z][A-Z_0-9]+) can inline the per-field rows into the generated alert-rule output tables. Co-Authored-By: Claude Opus 4.7 --- apps/docs/content/docs/en/tools/grafana.mdx | 145 ++++++++++---------- apps/sim/tools/grafana/create_alert_rule.ts | 5 +- apps/sim/tools/grafana/get_alert_rule.ts | 10 +- apps/sim/tools/grafana/list_alert_rules.ts | 11 +- apps/sim/tools/grafana/types.ts | 44 +++++- apps/sim/tools/grafana/update_alert_rule.ts | 6 +- apps/sim/tools/grafana/utils.ts | 44 ------ 7 files changed, 131 insertions(+), 134 deletions(-) diff --git a/apps/docs/content/docs/en/tools/grafana.mdx b/apps/docs/content/docs/en/tools/grafana.mdx index c09e2335fb4..47cbd9983d9 100644 --- a/apps/docs/content/docs/en/tools/grafana.mdx +++ b/apps/docs/content/docs/en/tools/grafana.mdx @@ -190,6 +190,26 @@ List all alert rules in the Grafana instance | Parameter | Type | Description | | --------- | ---- | ----------- | | `rules` | array | List of alert rules | +| ↳ `id` | number | Alert rule numeric ID | +| ↳ `uid` | string | Alert rule UID | +| ↳ `title` | string | Alert rule title | +| ↳ `condition` | string | RefId of the query used as the alert condition | +| ↳ `data` | json | Alert rule query/expression data array | +| ↳ `updated` | string | Last update timestamp | +| ↳ `noDataState` | string | State when no data is returned | +| ↳ `execErrState` | string | State on execution error | +| ↳ `for` | string | Duration the condition must hold before firing | +| ↳ `keepFiringFor` | string | Duration to keep firing after condition stops | +| ↳ `missingSeriesEvalsToResolve` | number | Number of missing series evaluations before resolving | +| ↳ `annotations` | json | Alert annotations | +| ↳ `labels` | json | Alert labels | +| ↳ `isPaused` | boolean | Whether the rule is paused | +| ↳ `folderUID` | string | Parent folder UID | +| ↳ `ruleGroup` | string | Rule group name | +| ↳ `orgID` | number | Organization ID | +| ↳ `provenance` | string | Provisioning source \(empty if API-managed\) | +| ↳ `notification_settings` | json | Per-rule notification settings \(overrides\) | +| ↳ `record` | json | Recording rule configuration \(recording rules only\) | ### `grafana_get_alert_rule` @@ -208,35 +228,26 @@ Get a specific alert rule by its UID | Parameter | Type | Description | | --------- | ---- | ----------- | -| `version` | string | Grafana version | -| `database` | string | Database health status | -| `status` | string | Health status | -| `dashboard` | json | Dashboard JSON | -| `meta` | json | Dashboard metadata | -| `dashboards` | json | List of dashboards | -| `uid` | string | Created/updated UID | -| `url` | string | Dashboard URL | -| `rules` | json | Alert rules list | -| `contactPoints` | json | Contact points list | -| `condition` | string | Alert condition refId | +| `id` | number | Alert rule numeric ID | +| `uid` | string | Alert rule UID | +| `title` | string | Alert rule title | +| `condition` | string | RefId of the query used as the alert condition | +| `data` | json | Alert rule query/expression data array | +| `updated` | string | Last update timestamp | +| `noDataState` | string | State when no data is returned | +| `execErrState` | string | State on execution error | | `for` | string | Duration the condition must hold before firing | -| `keepFiringFor` | string | Duration to keep firing after the condition stops | -| `missingSeriesEvalsToResolve` | number | Missing series evaluations before resolving | -| `isPaused` | boolean | Whether the alert rule is paused | +| `keepFiringFor` | string | Duration to keep firing after condition stops | +| `missingSeriesEvalsToResolve` | number | Number of missing series evaluations before resolving | +| `annotations` | json | Alert annotations | +| `labels` | json | Alert labels | +| `isPaused` | boolean | Whether the rule is paused | | `folderUID` | string | Parent folder UID | | `ruleGroup` | string | Rule group name | | `orgID` | number | Organization ID | -| `provenance` | string | Provisioning source | -| `noDataState` | string | State on no data | -| `execErrState` | string | State on execution error | -| `notification_settings` | json | Per-rule notification settings | -| `record` | json | Recording rule configuration | -| `updated` | string | Last update timestamp | -| `annotations` | json | Annotations list | -| `id` | number | Annotation ID | -| `dataSources` | json | Data sources list | -| `folders` | json | Folders list | -| `message` | string | Status message | +| `provenance` | string | Provisioning source \(empty if API-managed\) | +| `notification_settings` | json | Per-rule notification settings \(overrides\) | +| `record` | json | Recording rule configuration \(recording rules only\) | ### `grafana_create_alert_rule` @@ -271,35 +282,26 @@ Create a new alert rule | Parameter | Type | Description | | --------- | ---- | ----------- | -| `version` | string | Grafana version | -| `database` | string | Database health status | -| `status` | string | Health status | -| `dashboard` | json | Dashboard JSON | -| `meta` | json | Dashboard metadata | -| `dashboards` | json | List of dashboards | -| `uid` | string | Created/updated UID | -| `url` | string | Dashboard URL | -| `rules` | json | Alert rules list | -| `contactPoints` | json | Contact points list | -| `condition` | string | Alert condition refId | +| `id` | number | Alert rule numeric ID | +| `uid` | string | Alert rule UID | +| `title` | string | Alert rule title | +| `condition` | string | RefId of the query used as the alert condition | +| `data` | json | Alert rule query/expression data array | +| `updated` | string | Last update timestamp | +| `noDataState` | string | State when no data is returned | +| `execErrState` | string | State on execution error | | `for` | string | Duration the condition must hold before firing | -| `keepFiringFor` | string | Duration to keep firing after the condition stops | -| `missingSeriesEvalsToResolve` | number | Missing series evaluations before resolving | -| `isPaused` | boolean | Whether the alert rule is paused | +| `keepFiringFor` | string | Duration to keep firing after condition stops | +| `missingSeriesEvalsToResolve` | number | Number of missing series evaluations before resolving | +| `annotations` | json | Alert annotations | +| `labels` | json | Alert labels | +| `isPaused` | boolean | Whether the rule is paused | | `folderUID` | string | Parent folder UID | | `ruleGroup` | string | Rule group name | | `orgID` | number | Organization ID | -| `provenance` | string | Provisioning source | -| `noDataState` | string | State on no data | -| `execErrState` | string | State on execution error | -| `notification_settings` | json | Per-rule notification settings | -| `record` | json | Recording rule configuration | -| `updated` | string | Last update timestamp | -| `annotations` | json | Annotations list | -| `id` | number | Annotation ID | -| `dataSources` | json | Data sources list | -| `folders` | json | Folders list | -| `message` | string | Status message | +| `provenance` | string | Provisioning source \(empty if API-managed\) | +| `notification_settings` | json | Per-rule notification settings \(overrides\) | +| `record` | json | Recording rule configuration \(recording rules only\) | ### `grafana_update_alert_rule` @@ -320,7 +322,7 @@ Update an existing alert rule. Fetches the current rule and merges your changes. | `data` | string | No | New JSON array of query/expression data objects | | `forDuration` | string | No | Duration to wait before firing \(e.g., 5m, 1h\) | | `noDataState` | string | No | State when no data is returned \(NoData, Alerting, OK\) | -| `execErrState` | string | No | State on execution error \(Alerting, OK\) | +| `execErrState` | string | No | State on execution error \(Error, Alerting, OK\) | | `annotations` | string | No | JSON object of annotations | | `labels` | string | No | JSON object of labels | | `isPaused` | boolean | No | Whether the rule is paused | @@ -334,35 +336,26 @@ Update an existing alert rule. Fetches the current rule and merges your changes. | Parameter | Type | Description | | --------- | ---- | ----------- | -| `version` | string | Grafana version | -| `database` | string | Database health status | -| `status` | string | Health status | -| `dashboard` | json | Dashboard JSON | -| `meta` | json | Dashboard metadata | -| `dashboards` | json | List of dashboards | -| `uid` | string | Created/updated UID | -| `url` | string | Dashboard URL | -| `rules` | json | Alert rules list | -| `contactPoints` | json | Contact points list | -| `condition` | string | Alert condition refId | +| `id` | number | Alert rule numeric ID | +| `uid` | string | Alert rule UID | +| `title` | string | Alert rule title | +| `condition` | string | RefId of the query used as the alert condition | +| `data` | json | Alert rule query/expression data array | +| `updated` | string | Last update timestamp | +| `noDataState` | string | State when no data is returned | +| `execErrState` | string | State on execution error | | `for` | string | Duration the condition must hold before firing | -| `keepFiringFor` | string | Duration to keep firing after the condition stops | -| `missingSeriesEvalsToResolve` | number | Missing series evaluations before resolving | -| `isPaused` | boolean | Whether the alert rule is paused | +| `keepFiringFor` | string | Duration to keep firing after condition stops | +| `missingSeriesEvalsToResolve` | number | Number of missing series evaluations before resolving | +| `annotations` | json | Alert annotations | +| `labels` | json | Alert labels | +| `isPaused` | boolean | Whether the rule is paused | | `folderUID` | string | Parent folder UID | | `ruleGroup` | string | Rule group name | | `orgID` | number | Organization ID | -| `provenance` | string | Provisioning source | -| `noDataState` | string | State on no data | -| `execErrState` | string | State on execution error | -| `notification_settings` | json | Per-rule notification settings | -| `record` | json | Recording rule configuration | -| `updated` | string | Last update timestamp | -| `annotations` | json | Annotations list | -| `id` | number | Annotation ID | -| `dataSources` | json | Data sources list | -| `folders` | json | Folders list | -| `message` | string | Status message | +| `provenance` | string | Provisioning source \(empty if API-managed\) | +| `notification_settings` | json | Per-rule notification settings \(overrides\) | +| `record` | json | Recording rule configuration \(recording rules only\) | ### `grafana_delete_alert_rule` diff --git a/apps/sim/tools/grafana/create_alert_rule.ts b/apps/sim/tools/grafana/create_alert_rule.ts index 27597126317..0c07eea7683 100644 --- a/apps/sim/tools/grafana/create_alert_rule.ts +++ b/apps/sim/tools/grafana/create_alert_rule.ts @@ -2,7 +2,8 @@ import type { GrafanaCreateAlertRuleParams, GrafanaCreateAlertRuleResponse, } from '@/tools/grafana/types' -import { alertRuleOutputFields, mapAlertRule } from '@/tools/grafana/utils' +import { ALERT_RULE_OUTPUT_FIELDS } from '@/tools/grafana/types' +import { mapAlertRule } from '@/tools/grafana/utils' import type { ToolConfig } from '@/tools/types' export const createAlertRuleTool: ToolConfig< @@ -222,5 +223,5 @@ export const createAlertRuleTool: ToolConfig< return { success: true, output: mapAlertRule(data) } }, - outputs: alertRuleOutputFields, + outputs: ALERT_RULE_OUTPUT_FIELDS, } diff --git a/apps/sim/tools/grafana/get_alert_rule.ts b/apps/sim/tools/grafana/get_alert_rule.ts index c8701cb09b1..1389872c620 100644 --- a/apps/sim/tools/grafana/get_alert_rule.ts +++ b/apps/sim/tools/grafana/get_alert_rule.ts @@ -1,5 +1,9 @@ -import type { GrafanaGetAlertRuleParams, GrafanaGetAlertRuleResponse } from '@/tools/grafana/types' -import { alertRuleOutputFields, mapAlertRule } from '@/tools/grafana/utils' +import { + ALERT_RULE_OUTPUT_FIELDS, + type GrafanaGetAlertRuleParams, + type GrafanaGetAlertRuleResponse, +} from '@/tools/grafana/types' +import { mapAlertRule } from '@/tools/grafana/utils' import type { ToolConfig } from '@/tools/types' export const getAlertRuleTool: ToolConfig = @@ -57,5 +61,5 @@ export const getAlertRuleTool: ToolConfig = { + id: { type: 'number', description: 'Alert rule numeric ID', optional: true }, + uid: { type: 'string', description: 'Alert rule UID' }, + title: { type: 'string', description: 'Alert rule title' }, + condition: { type: 'string', description: 'RefId of the query used as the alert condition' }, + data: { type: 'json', description: 'Alert rule query/expression data array' }, + updated: { type: 'string', description: 'Last update timestamp', optional: true }, + noDataState: { type: 'string', description: 'State when no data is returned' }, + execErrState: { type: 'string', description: 'State on execution error' }, + for: { type: 'string', description: 'Duration the condition must hold before firing' }, + keepFiringFor: { + type: 'string', + description: 'Duration to keep firing after condition stops', + optional: true, + }, + missingSeriesEvalsToResolve: { + type: 'number', + description: 'Number of missing series evaluations before resolving', + optional: true, + }, + annotations: { type: 'json', description: 'Alert annotations' }, + labels: { type: 'json', description: 'Alert labels' }, + isPaused: { type: 'boolean', description: 'Whether the rule is paused' }, + folderUID: { type: 'string', description: 'Parent folder UID' }, + ruleGroup: { type: 'string', description: 'Rule group name' }, + orgID: { type: 'number', description: 'Organization ID' }, + provenance: { type: 'string', description: 'Provisioning source (empty if API-managed)' }, + notification_settings: { + type: 'json', + description: 'Per-rule notification settings (overrides)', + optional: true, + }, + record: { + type: 'json', + description: 'Recording rule configuration (recording rules only)', + optional: true, + }, +} // Common parameters for all Grafana tools interface GrafanaBaseParams { diff --git a/apps/sim/tools/grafana/update_alert_rule.ts b/apps/sim/tools/grafana/update_alert_rule.ts index 1fcff72a880..9ca23bff773 100644 --- a/apps/sim/tools/grafana/update_alert_rule.ts +++ b/apps/sim/tools/grafana/update_alert_rule.ts @@ -1,5 +1,5 @@ -import type { GrafanaUpdateAlertRuleParams } from '@/tools/grafana/types' -import { alertRuleOutputFields, mapAlertRule } from '@/tools/grafana/utils' +import { ALERT_RULE_OUTPUT_FIELDS, type GrafanaUpdateAlertRuleParams } from '@/tools/grafana/types' +import { mapAlertRule } from '@/tools/grafana/utils' import type { ToolConfig, ToolResponse } from '@/tools/types' // Using ToolResponse for intermediate state since this tool fetches existing data first @@ -291,5 +291,5 @@ export const updateAlertRuleTool: ToolConfig) { record: (rule.record as Record) ?? null, } } - -/** - * Canonical output schema fields shared across alert rule tools. - */ -export const alertRuleOutputFields: Record = { - id: { type: 'number', description: 'Alert rule numeric ID', optional: true }, - uid: { type: 'string', description: 'Alert rule UID' }, - title: { type: 'string', description: 'Alert rule title' }, - condition: { type: 'string', description: 'RefId of the query used as the alert condition' }, - data: { type: 'json', description: 'Alert rule query/expression data array' }, - updated: { type: 'string', description: 'Last update timestamp', optional: true }, - noDataState: { type: 'string', description: 'State when no data is returned' }, - execErrState: { type: 'string', description: 'State on execution error' }, - for: { type: 'string', description: 'Duration the condition must hold before firing' }, - keepFiringFor: { - type: 'string', - description: 'Duration to keep firing after condition stops', - optional: true, - }, - missingSeriesEvalsToResolve: { - type: 'number', - description: 'Number of missing series evaluations before resolving', - optional: true, - }, - annotations: { type: 'json', description: 'Alert annotations' }, - labels: { type: 'json', description: 'Alert labels' }, - isPaused: { type: 'boolean', description: 'Whether the rule is paused' }, - folderUID: { type: 'string', description: 'Parent folder UID' }, - ruleGroup: { type: 'string', description: 'Rule group name' }, - orgID: { type: 'number', description: 'Organization ID' }, - provenance: { type: 'string', description: 'Provisioning source (empty if API-managed)' }, - notification_settings: { - type: 'json', - description: 'Per-rule notification settings (overrides)', - optional: true, - }, - record: { - type: 'json', - description: 'Recording rule configuration (recording rules only)', - optional: true, - }, -}