From 044612def2edd78630130fe4ce131ebba33ba154 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Tue, 7 Apr 2026 13:15:53 -0700 Subject: [PATCH 1/7] feat(block): Add cloudwatch publish operation --- .../tools/cloudwatch/put-metric-data/route.ts | 116 ++++++++++++++++++ apps/sim/blocks/blocks/cloudwatch.ts | 116 +++++++++++++++++- apps/sim/tools/cloudwatch/index.ts | 2 + apps/sim/tools/cloudwatch/put_metric_data.ts | 106 ++++++++++++++++ apps/sim/tools/cloudwatch/types.ts | 19 +++ apps/sim/tools/registry.ts | 2 + 6 files changed, 355 insertions(+), 6 deletions(-) create mode 100644 apps/sim/app/api/tools/cloudwatch/put-metric-data/route.ts create mode 100644 apps/sim/tools/cloudwatch/put_metric_data.ts diff --git a/apps/sim/app/api/tools/cloudwatch/put-metric-data/route.ts b/apps/sim/app/api/tools/cloudwatch/put-metric-data/route.ts new file mode 100644 index 00000000000..bea2b2f1b16 --- /dev/null +++ b/apps/sim/app/api/tools/cloudwatch/put-metric-data/route.ts @@ -0,0 +1,116 @@ +import { + CloudWatchClient, + PutMetricDataCommand, + type StandardUnit, +} from '@aws-sdk/client-cloudwatch' +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { checkInternalAuth } from '@/lib/auth/hybrid' + +const logger = createLogger('CloudWatchPutMetricData') + +const VALID_UNITS = [ + 'Seconds', + 'Microseconds', + 'Milliseconds', + 'Bytes', + 'Kilobytes', + 'Megabytes', + 'Gigabytes', + 'Terabytes', + 'Bits', + 'Kilobits', + 'Megabits', + 'Gigabits', + 'Terabits', + 'Percent', + 'Count', + 'Bytes/Second', + 'Kilobytes/Second', + 'Megabytes/Second', + 'Gigabytes/Second', + 'Terabytes/Second', + 'Bits/Second', + 'Kilobits/Second', + 'Megabits/Second', + 'Gigabits/Second', + 'Terabits/Second', + 'Count/Second', + 'None', +] as const + +const PutMetricDataSchema = z.object({ + region: z.string().min(1, 'AWS region is required'), + accessKeyId: z.string().min(1, 'AWS access key ID is required'), + secretAccessKey: z.string().min(1, 'AWS secret access key is required'), + namespace: z.string().min(1, 'Namespace is required'), + metricName: z.string().min(1, 'Metric name is required'), + value: z.number({ coerce: true }), + unit: z.enum(VALID_UNITS).optional(), + dimensions: z.string().optional(), +}) + +export async function POST(request: NextRequest) { + try { + const auth = await checkInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const body = await request.json() + const validatedData = PutMetricDataSchema.parse(body) + + const client = new CloudWatchClient({ + region: validatedData.region, + credentials: { + accessKeyId: validatedData.accessKeyId, + secretAccessKey: validatedData.secretAccessKey, + }, + }) + + const timestamp = new Date() + + const dimensions: { Name: string; Value: string }[] = [] + if (validatedData.dimensions) { + const parsed = JSON.parse(validatedData.dimensions) + if (typeof parsed === 'object' && parsed !== null) { + for (const [name, value] of Object.entries(parsed)) { + dimensions.push({ Name: name, Value: String(value) }) + } + } + } + + const command = new PutMetricDataCommand({ + Namespace: validatedData.namespace, + MetricData: [ + { + MetricName: validatedData.metricName, + Value: validatedData.value, + Timestamp: timestamp, + ...(validatedData.unit && { Unit: validatedData.unit as StandardUnit }), + ...(dimensions.length > 0 && { Dimensions: dimensions }), + }, + ], + }) + + await client.send(command) + + return NextResponse.json({ + success: true, + output: { + success: true, + namespace: validatedData.namespace, + metricName: validatedData.metricName, + value: validatedData.value, + unit: validatedData.unit ?? 'None', + timestamp: timestamp.toISOString(), + }, + }) + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : 'Failed to publish CloudWatch metric' + logger.error('PutMetricData failed', { error: errorMessage }) + return NextResponse.json({ error: errorMessage }, { status: 500 }) + } +} diff --git a/apps/sim/blocks/blocks/cloudwatch.ts b/apps/sim/blocks/blocks/cloudwatch.ts index c68bcf29430..437f6ea28ac 100644 --- a/apps/sim/blocks/blocks/cloudwatch.ts +++ b/apps/sim/blocks/blocks/cloudwatch.ts @@ -8,6 +8,7 @@ import type { CloudWatchGetLogEventsResponse, CloudWatchGetMetricStatisticsResponse, CloudWatchListMetricsResponse, + CloudWatchPutMetricDataResponse, CloudWatchQueryLogsResponse, } from '@/tools/cloudwatch/types' @@ -19,6 +20,7 @@ export const CloudWatchBlock: BlockConfig< | CloudWatchDescribeAlarmsResponse | CloudWatchListMetricsResponse | CloudWatchGetMetricStatisticsResponse + | CloudWatchPutMetricDataResponse > = { type: 'cloudwatch', name: 'CloudWatch', @@ -42,6 +44,7 @@ export const CloudWatchBlock: BlockConfig< { label: 'Describe Log Streams', id: 'describe_log_streams' }, { label: 'List Metrics', id: 'list_metrics' }, { label: 'Get Metric Statistics', id: 'get_metric_statistics' }, + { label: 'Publish Metric', id: 'put_metric_data' }, { label: 'Describe Alarms', id: 'describe_alarms' }, ], value: () => 'query_logs', @@ -203,17 +206,29 @@ Return ONLY the query — no explanations, no markdown code blocks.`, id: 'metricNamespace', title: 'Namespace', type: 'short-input', - placeholder: 'e.g., AWS/EC2, AWS/Lambda, AWS/RDS', - condition: { field: 'operation', value: ['list_metrics', 'get_metric_statistics'] }, - required: { field: 'operation', value: 'get_metric_statistics' }, + placeholder: 'e.g., AWS/EC2, AWS/Lambda, Custom/MyApp', + condition: { + field: 'operation', + value: ['list_metrics', 'get_metric_statistics', 'put_metric_data'], + }, + required: { + field: 'operation', + value: ['get_metric_statistics', 'put_metric_data'], + }, }, { id: 'metricName', title: 'Metric Name', type: 'short-input', - placeholder: 'e.g., CPUUtilization, Invocations', - condition: { field: 'operation', value: ['list_metrics', 'get_metric_statistics'] }, - required: { field: 'operation', value: 'get_metric_statistics' }, + placeholder: 'e.g., CPUUtilization, Invocations, ErrorCount', + condition: { + field: 'operation', + value: ['list_metrics', 'get_metric_statistics', 'put_metric_data'], + }, + required: { + field: 'operation', + value: ['get_metric_statistics', 'put_metric_data'], + }, }, { id: 'recentlyActive', @@ -221,6 +236,44 @@ Return ONLY the query — no explanations, no markdown code blocks.`, type: 'switch', condition: { field: 'operation', value: 'list_metrics' }, }, + // Publish Metric fields + { + id: 'metricValue', + title: 'Value', + type: 'short-input', + placeholder: 'e.g., 1, 42.5', + condition: { field: 'operation', value: 'put_metric_data' }, + required: { field: 'operation', value: 'put_metric_data' }, + }, + { + id: 'metricUnit', + title: 'Unit', + type: 'dropdown', + options: [ + { label: 'None', id: 'None' }, + { label: 'Count', id: 'Count' }, + { label: 'Percent', id: 'Percent' }, + { label: 'Seconds', id: 'Seconds' }, + { label: 'Milliseconds', id: 'Milliseconds' }, + { label: 'Microseconds', id: 'Microseconds' }, + { label: 'Bytes', id: 'Bytes' }, + { label: 'Kilobytes', id: 'Kilobytes' }, + { label: 'Megabytes', id: 'Megabytes' }, + { label: 'Gigabytes', id: 'Gigabytes' }, + { label: 'Bits', id: 'Bits' }, + { label: 'Bytes/Second', id: 'Bytes/Second' }, + { label: 'Count/Second', id: 'Count/Second' }, + ], + value: () => 'None', + condition: { field: 'operation', value: 'put_metric_data' }, + }, + { + id: 'publishDimensions', + title: 'Dimensions', + type: 'table', + columns: ['name', 'value'], + condition: { field: 'operation', value: 'put_metric_data' }, + }, // Get Metric Statistics fields { id: 'metricPeriod', @@ -309,6 +362,7 @@ Return ONLY the query — no explanations, no markdown code blocks.`, 'cloudwatch_describe_log_streams', 'cloudwatch_list_metrics', 'cloudwatch_get_metric_statistics', + 'cloudwatch_put_metric_data', 'cloudwatch_describe_alarms', ], config: { @@ -326,6 +380,8 @@ Return ONLY the query — no explanations, no markdown code blocks.`, return 'cloudwatch_list_metrics' case 'get_metric_statistics': return 'cloudwatch_get_metric_statistics' + case 'put_metric_data': + return 'cloudwatch_put_metric_data' case 'describe_alarms': return 'cloudwatch_describe_alarms' default: @@ -479,6 +535,44 @@ Return ONLY the query — no explanations, no markdown code blocks.`, } } + case 'put_metric_data': { + if (!rest.metricNamespace) { + throw new Error('Namespace is required') + } + if (!rest.metricName) { + throw new Error('Metric name is required') + } + if (rest.metricValue === undefined || rest.metricValue === '') { + throw new Error('Metric value is required') + } + + return { + awsRegion, + awsAccessKeyId, + awsSecretAccessKey, + namespace: rest.metricNamespace, + metricName: rest.metricName, + value: Number(rest.metricValue), + ...(rest.metricUnit && rest.metricUnit !== 'None' && { unit: rest.metricUnit }), + ...(rest.publishDimensions && { + dimensions: (() => { + const dims = rest.publishDimensions + if (typeof dims === 'string') return dims + if (Array.isArray(dims)) { + const obj: Record = {} + for (const row of dims) { + const name = row.cells?.name + const value = row.cells?.value + if (name && value !== undefined) obj[name] = String(value) + } + return JSON.stringify(obj) + } + return JSON.stringify(dims) + })(), + }), + } + } + case 'describe_alarms': return { awsRegion, @@ -518,6 +612,12 @@ Return ONLY the query — no explanations, no markdown code blocks.`, metricPeriod: { type: 'number', description: 'Granularity in seconds' }, metricStatistics: { type: 'string', description: 'Statistic type (Average, Sum, etc.)' }, metricDimensions: { type: 'json', description: 'Metric dimensions (Name/Value pairs)' }, + metricValue: { type: 'number', description: 'Metric value to publish' }, + metricUnit: { type: 'string', description: 'Metric unit (Count, Seconds, Bytes, etc.)' }, + publishDimensions: { + type: 'json', + description: 'Dimensions for published metric (Name/Value pairs)', + }, alarmNamePrefix: { type: 'string', description: 'Alarm name prefix filter' }, stateValue: { type: 'string', @@ -567,5 +667,9 @@ Return ONLY the query — no explanations, no markdown code blocks.`, type: 'array', description: 'CloudWatch alarms with state and configuration', }, + timestamp: { + type: 'string', + description: 'Timestamp when metric was published', + }, }, } diff --git a/apps/sim/tools/cloudwatch/index.ts b/apps/sim/tools/cloudwatch/index.ts index 4ce796e168d..bcc0b49e7ab 100644 --- a/apps/sim/tools/cloudwatch/index.ts +++ b/apps/sim/tools/cloudwatch/index.ts @@ -4,6 +4,7 @@ import { describeLogStreamsTool } from '@/tools/cloudwatch/describe_log_streams' import { getLogEventsTool } from '@/tools/cloudwatch/get_log_events' import { getMetricStatisticsTool } from '@/tools/cloudwatch/get_metric_statistics' import { listMetricsTool } from '@/tools/cloudwatch/list_metrics' +import { putMetricDataTool } from '@/tools/cloudwatch/put_metric_data' import { queryLogsTool } from '@/tools/cloudwatch/query_logs' export const cloudwatchDescribeAlarmsTool = describeAlarmsTool @@ -12,4 +13,5 @@ export const cloudwatchDescribeLogStreamsTool = describeLogStreamsTool export const cloudwatchGetLogEventsTool = getLogEventsTool export const cloudwatchGetMetricStatisticsTool = getMetricStatisticsTool export const cloudwatchListMetricsTool = listMetricsTool +export const cloudwatchPutMetricDataTool = putMetricDataTool export const cloudwatchQueryLogsTool = queryLogsTool diff --git a/apps/sim/tools/cloudwatch/put_metric_data.ts b/apps/sim/tools/cloudwatch/put_metric_data.ts new file mode 100644 index 00000000000..4449be512fb --- /dev/null +++ b/apps/sim/tools/cloudwatch/put_metric_data.ts @@ -0,0 +1,106 @@ +import type { + CloudWatchPutMetricDataParams, + CloudWatchPutMetricDataResponse, +} from '@/tools/cloudwatch/types' +import type { ToolConfig } from '@/tools/types' + +export const putMetricDataTool: ToolConfig< + CloudWatchPutMetricDataParams, + CloudWatchPutMetricDataResponse +> = { + id: 'cloudwatch_put_metric_data', + name: 'CloudWatch Publish Metric', + description: 'Publish a custom metric data point to CloudWatch', + version: '1.0', + + params: { + awsRegion: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'AWS region (e.g., us-east-1)', + }, + awsAccessKeyId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'AWS access key ID', + }, + awsSecretAccessKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'AWS secret access key', + }, + namespace: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Metric namespace (e.g., Custom/MyApp)', + }, + metricName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Name of the metric', + }, + value: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'Metric value to publish', + }, + unit: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Unit of the metric (e.g., Count, Seconds, Bytes)', + }, + dimensions: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'JSON string of dimension name/value pairs', + }, + }, + + request: { + url: '/api/tools/cloudwatch/put-metric-data', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + region: params.awsRegion, + accessKeyId: params.awsAccessKeyId, + secretAccessKey: params.awsSecretAccessKey, + namespace: params.namespace, + metricName: params.metricName, + value: params.value, + ...(params.unit && { unit: params.unit }), + ...(params.dimensions && { dimensions: params.dimensions }), + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error || 'Failed to publish CloudWatch metric') + } + + return { + success: true, + output: data.output, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Whether the metric was published successfully' }, + namespace: { type: 'string', description: 'Metric namespace' }, + metricName: { type: 'string', description: 'Metric name' }, + value: { type: 'number', description: 'Published metric value' }, + unit: { type: 'string', description: 'Metric unit' }, + timestamp: { type: 'string', description: 'Timestamp when the metric was published' }, + }, +} diff --git a/apps/sim/tools/cloudwatch/types.ts b/apps/sim/tools/cloudwatch/types.ts index 32259fa5a08..be632203b8b 100644 --- a/apps/sim/tools/cloudwatch/types.ts +++ b/apps/sim/tools/cloudwatch/types.ts @@ -144,3 +144,22 @@ export interface CloudWatchDescribeAlarmsResponse extends ToolResponse { }[] } } + +export interface CloudWatchPutMetricDataParams extends CloudWatchConnectionConfig { + namespace: string + metricName: string + value: number + unit?: string + dimensions?: string +} + +export interface CloudWatchPutMetricDataResponse extends ToolResponse { + output: { + success: boolean + namespace: string + metricName: string + value: number + unit: string + timestamp: string + } +} diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index f094eddcdf9..fdf89bc9520 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -301,6 +301,7 @@ import { cloudwatchGetLogEventsTool, cloudwatchGetMetricStatisticsTool, cloudwatchListMetricsTool, + cloudwatchPutMetricDataTool, cloudwatchQueryLogsTool, } from '@/tools/cloudwatch' import { @@ -3457,6 +3458,7 @@ export const tools: Record = { cloudwatch_get_log_events: cloudwatchGetLogEventsTool, cloudwatch_list_metrics: cloudwatchListMetricsTool, cloudwatch_get_metric_statistics: cloudwatchGetMetricStatisticsTool, + cloudwatch_put_metric_data: cloudwatchPutMetricDataTool, dynamodb_get: dynamodbGetTool, dynamodb_put: dynamodbPutTool, dynamodb_query: dynamodbQueryTool, From f8cbbe7c4e6515a09e8d7b403ae3643243f6e61d Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 8 Apr 2026 16:23:00 -0700 Subject: [PATCH 2/7] fix(integrations): validate and fix cloudwatch, cloudformation, athena conventions - Update tool version strings from '1.0' to '1.0.0' across all three integrations - Add missing `export * from './types'` barrel re-exports (cloudwatch, cloudformation) - Add docsLink, wandConfig timestamps, mode: 'advanced' on optional fields (cloudwatch) - Add dropdown defaults, ZodError handling, docs intro section (cloudwatch) - Add mode: 'advanced' on limit field (cloudformation) - Alphabetize registry entries (cloudwatch, cloudformation) - Fix athena docs maxResults range (1-999) Co-Authored-By: Claude Opus 4.6 --- apps/docs/content/docs/en/tools/athena.mdx | 2 +- .../docs/content/docs/en/tools/cloudwatch.mdx | 46 +++++++++++++++++++ .../integrations/data/integrations.json | 6 ++- .../tools/cloudwatch/put-metric-data/route.ts | 6 +++ apps/sim/blocks/blocks/cloudformation.ts | 1 + apps/sim/blocks/blocks/cloudwatch.ts | 31 +++++++++---- apps/sim/tools/athena/create_named_query.ts | 2 +- apps/sim/tools/athena/get_named_query.ts | 2 +- apps/sim/tools/athena/get_query_execution.ts | 2 +- apps/sim/tools/athena/get_query_results.ts | 2 +- apps/sim/tools/athena/list_named_queries.ts | 2 +- .../sim/tools/athena/list_query_executions.ts | 2 +- apps/sim/tools/athena/start_query.ts | 2 +- apps/sim/tools/athena/stop_query.ts | 2 +- .../describe_stack_drift_detection_status.ts | 2 +- .../cloudformation/describe_stack_events.ts | 2 +- .../tools/cloudformation/describe_stacks.ts | 2 +- .../cloudformation/detect_stack_drift.ts | 2 +- apps/sim/tools/cloudformation/get_template.ts | 2 +- apps/sim/tools/cloudformation/index.ts | 2 + .../cloudformation/list_stack_resources.ts | 2 +- .../tools/cloudformation/validate_template.ts | 2 +- apps/sim/tools/cloudwatch/describe_alarms.ts | 2 +- .../tools/cloudwatch/describe_log_groups.ts | 2 +- .../tools/cloudwatch/describe_log_streams.ts | 2 +- apps/sim/tools/cloudwatch/get_log_events.ts | 2 +- .../tools/cloudwatch/get_metric_statistics.ts | 2 +- apps/sim/tools/cloudwatch/index.ts | 2 + apps/sim/tools/cloudwatch/list_metrics.ts | 2 +- apps/sim/tools/cloudwatch/put_metric_data.ts | 2 +- apps/sim/tools/cloudwatch/query_logs.ts | 2 +- apps/sim/tools/registry.ts | 12 ++--- 32 files changed, 113 insertions(+), 41 deletions(-) diff --git a/apps/docs/content/docs/en/tools/athena.mdx b/apps/docs/content/docs/en/tools/athena.mdx index d77394bcefc..698188291f6 100644 --- a/apps/docs/content/docs/en/tools/athena.mdx +++ b/apps/docs/content/docs/en/tools/athena.mdx @@ -113,7 +113,7 @@ Retrieve the results of a completed Athena query execution | `awsAccessKeyId` | string | Yes | AWS access key ID | | `awsSecretAccessKey` | string | Yes | AWS secret access key | | `queryExecutionId` | string | Yes | Query execution ID to get results for | -| `maxResults` | number | No | Maximum number of rows to return \(1-1000\) | +| `maxResults` | number | No | Maximum number of rows to return \(1-999\) | | `nextToken` | string | No | Pagination token from a previous request | #### Output diff --git a/apps/docs/content/docs/en/tools/cloudwatch.mdx b/apps/docs/content/docs/en/tools/cloudwatch.mdx index a3c5757a87a..1fc1b19ea75 100644 --- a/apps/docs/content/docs/en/tools/cloudwatch.mdx +++ b/apps/docs/content/docs/en/tools/cloudwatch.mdx @@ -10,6 +10,24 @@ import { BlockInfoCard } from "@/components/ui/block-info-card" color="linear-gradient(45deg, #B0084D 0%, #FF4F8B 100%)" /> +{/* MANUAL-CONTENT-START:intro */} +[AWS CloudWatch](https://aws.amazon.com/cloudwatch/) is a monitoring and observability service that provides data and actionable insights for AWS resources, applications, and services. CloudWatch collects monitoring and operational data in the form of logs, metrics, and events, giving you a unified view of your AWS environment. + +With the CloudWatch integration, you can: + +- **Query Logs (Insights)**: Run CloudWatch Log Insights queries against one or more log groups to analyze log data with a powerful query language +- **Describe Log Groups**: List available CloudWatch log groups in your account, optionally filtered by name prefix +- **Get Log Events**: Retrieve log events from a specific log stream within a log group +- **Describe Log Streams**: List log streams within a log group, ordered by last event time or filtered by name prefix +- **List Metrics**: Browse available CloudWatch metrics, optionally filtered by namespace, metric name, or recent activity +- **Get Metric Statistics**: Retrieve statistical data for a metric over a specified time range with configurable granularity +- **Publish Metric**: Publish custom metric data points to CloudWatch for your own application monitoring +- **Describe Alarms**: List and filter CloudWatch alarms by name prefix, state, or alarm type + +In Sim, the CloudWatch integration enables your agents to monitor AWS infrastructure, analyze application logs, track custom metrics, and respond to alarm states as part of automated DevOps and SRE workflows. This is especially powerful when combined with other AWS integrations like CloudFormation and SNS for end-to-end infrastructure management. +{/* MANUAL-CONTENT-END */} + + ## Usage Instructions Integrate AWS CloudWatch into workflows. Run Log Insights queries, list log groups, retrieve log events, list and get metrics, and monitor alarms. Requires AWS access key and secret access key. @@ -155,6 +173,34 @@ Get statistics for a CloudWatch metric over a time range | `label` | string | Metric label | | `datapoints` | array | Datapoints with timestamp and statistics values | +### `cloudwatch_put_metric_data` + +Publish a custom metric data point to CloudWatch + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `awsRegion` | string | Yes | AWS region \(e.g., us-east-1\) | +| `awsAccessKeyId` | string | Yes | AWS access key ID | +| `awsSecretAccessKey` | string | Yes | AWS secret access key | +| `namespace` | string | Yes | Metric namespace \(e.g., Custom/MyApp\) | +| `metricName` | string | Yes | Name of the metric | +| `value` | number | Yes | Metric value to publish | +| `unit` | string | No | Unit of the metric \(e.g., Count, Seconds, Bytes\) | +| `dimensions` | string | No | JSON string of dimension name/value pairs | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the metric was published successfully | +| `namespace` | string | Metric namespace | +| `metricName` | string | Metric name | +| `value` | number | Published metric value | +| `unit` | string | Metric unit | +| `timestamp` | string | Timestamp when the metric was published | + ### `cloudwatch_describe_alarms` List and filter CloudWatch alarms diff --git a/apps/sim/app/(landing)/integrations/data/integrations.json b/apps/sim/app/(landing)/integrations/data/integrations.json index e6658415998..a05fcbb7eff 100644 --- a/apps/sim/app/(landing)/integrations/data/integrations.json +++ b/apps/sim/app/(landing)/integrations/data/integrations.json @@ -2044,12 +2044,16 @@ "name": "Get Metric Statistics", "description": "Get statistics for a CloudWatch metric over a time range" }, + { + "name": "Publish Metric", + "description": "Publish a custom metric data point to CloudWatch" + }, { "name": "Describe Alarms", "description": "List and filter CloudWatch alarms" } ], - "operationCount": 7, + "operationCount": 8, "triggers": [], "triggerCount": 0, "authType": "none", diff --git a/apps/sim/app/api/tools/cloudwatch/put-metric-data/route.ts b/apps/sim/app/api/tools/cloudwatch/put-metric-data/route.ts index bea2b2f1b16..fe0e7473443 100644 --- a/apps/sim/app/api/tools/cloudwatch/put-metric-data/route.ts +++ b/apps/sim/app/api/tools/cloudwatch/put-metric-data/route.ts @@ -108,6 +108,12 @@ export async function POST(request: NextRequest) { }, }) } catch (error) { + if (error instanceof z.ZodError) { + return NextResponse.json( + { error: error.errors[0]?.message ?? 'Invalid request' }, + { status: 400 } + ) + } const errorMessage = error instanceof Error ? error.message : 'Failed to publish CloudWatch metric' logger.error('PutMetricData failed', { error: errorMessage }) diff --git a/apps/sim/blocks/blocks/cloudformation.ts b/apps/sim/blocks/blocks/cloudformation.ts index d99fabebac2..ab5456d4c52 100644 --- a/apps/sim/blocks/blocks/cloudformation.ts +++ b/apps/sim/blocks/blocks/cloudformation.ts @@ -117,6 +117,7 @@ export const CloudFormationBlock: BlockConfig< type: 'short-input', placeholder: '50', condition: { field: 'operation', value: 'describe_stack_events' }, + mode: 'advanced', }, ], tools: { diff --git a/apps/sim/blocks/blocks/cloudwatch.ts b/apps/sim/blocks/blocks/cloudwatch.ts index 437f6ea28ac..0b6ecfba302 100644 --- a/apps/sim/blocks/blocks/cloudwatch.ts +++ b/apps/sim/blocks/blocks/cloudwatch.ts @@ -29,6 +29,7 @@ export const CloudWatchBlock: BlockConfig< 'Integrate AWS CloudWatch into workflows. Run Log Insights queries, list log groups, retrieve log events, list and get metrics, and monitor alarms. Requires AWS access key and secret access key.', category: 'tools', integrationType: IntegrationType.Analytics, + docsLink: 'https://docs.sim.ai/tools/cloudwatch', tags: ['cloud', 'monitoring'], bgColor: 'linear-gradient(45deg, #B0084D 0%, #FF4F8B 100%)', icon: CloudWatchIcon, @@ -72,7 +73,6 @@ export const CloudWatchBlock: BlockConfig< password: true, required: true, }, - // Query Logs fields { id: 'logGroupSelector', title: 'Log Group', @@ -127,6 +127,14 @@ Return ONLY the query — no explanations, no markdown code blocks.`, value: ['query_logs', 'get_log_events', 'get_metric_statistics'], }, required: { field: 'operation', value: ['query_logs', 'get_metric_statistics'] }, + wandConfig: { + enabled: true, + prompt: `Generate a Unix epoch timestamp (in seconds) based on the user's description of a point in time. + +Return ONLY the numeric timestamp - no explanations, no quotes, no extra text.`, + placeholder: 'Describe the start time (e.g., "1 hour ago", "beginning of today")...', + generationType: 'timestamp', + }, }, { id: 'endTime', @@ -138,8 +146,15 @@ Return ONLY the query — no explanations, no markdown code blocks.`, value: ['query_logs', 'get_log_events', 'get_metric_statistics'], }, required: { field: 'operation', value: ['query_logs', 'get_metric_statistics'] }, + wandConfig: { + enabled: true, + prompt: `Generate a Unix epoch timestamp (in seconds) based on the user's description of a point in time. + +Return ONLY the numeric timestamp - no explanations, no quotes, no extra text.`, + placeholder: 'Describe the end time (e.g., "now", "end of yesterday")...', + generationType: 'timestamp', + }, }, - // Describe Log Groups fields { id: 'prefix', title: 'Log Group Name Prefix', @@ -147,7 +162,6 @@ Return ONLY the query — no explanations, no markdown code blocks.`, placeholder: '/aws/lambda/', condition: { field: 'operation', value: 'describe_log_groups' }, }, - // Get Log Events / Describe Log Streams — shared log group selector { id: 'logGroupNameSelector', title: 'Log Group', @@ -170,7 +184,6 @@ Return ONLY the query — no explanations, no markdown code blocks.`, required: { field: 'operation', value: ['get_log_events', 'describe_log_streams'] }, mode: 'advanced', }, - // Describe Log Streams — stream prefix filter { id: 'streamPrefix', title: 'Stream Name Prefix', @@ -178,7 +191,6 @@ Return ONLY the query — no explanations, no markdown code blocks.`, placeholder: '2024/03/31/', condition: { field: 'operation', value: 'describe_log_streams' }, }, - // Get Log Events — log stream selector (cascading: depends on log group) { id: 'logStreamNameSelector', title: 'Log Stream', @@ -201,7 +213,6 @@ Return ONLY the query — no explanations, no markdown code blocks.`, required: { field: 'operation', value: 'get_log_events' }, mode: 'advanced', }, - // List Metrics fields { id: 'metricNamespace', title: 'Namespace', @@ -235,8 +246,8 @@ Return ONLY the query — no explanations, no markdown code blocks.`, title: 'Recently Active Only', type: 'switch', condition: { field: 'operation', value: 'list_metrics' }, + mode: 'advanced', }, - // Publish Metric fields { id: 'metricValue', title: 'Value', @@ -274,7 +285,6 @@ Return ONLY the query — no explanations, no markdown code blocks.`, columns: ['name', 'value'], condition: { field: 'operation', value: 'put_metric_data' }, }, - // Get Metric Statistics fields { id: 'metricPeriod', title: 'Period (seconds)', @@ -304,7 +314,6 @@ Return ONLY the query — no explanations, no markdown code blocks.`, columns: ['name', 'value'], condition: { field: 'operation', value: 'get_metric_statistics' }, }, - // Describe Alarms fields { id: 'alarmNamePrefix', title: 'Alarm Name Prefix', @@ -322,6 +331,7 @@ Return ONLY the query — no explanations, no markdown code blocks.`, { label: 'ALARM', id: 'ALARM' }, { label: 'INSUFFICIENT_DATA', id: 'INSUFFICIENT_DATA' }, ], + value: () => '', condition: { field: 'operation', value: 'describe_alarms' }, }, { @@ -333,9 +343,9 @@ Return ONLY the query — no explanations, no markdown code blocks.`, { label: 'Metric Alarm', id: 'MetricAlarm' }, { label: 'Composite Alarm', id: 'CompositeAlarm' }, ], + value: () => '', condition: { field: 'operation', value: 'describe_alarms' }, }, - // Shared limit field { id: 'limit', title: 'Limit', @@ -352,6 +362,7 @@ Return ONLY the query — no explanations, no markdown code blocks.`, 'describe_alarms', ], }, + mode: 'advanced', }, ], tools: { diff --git a/apps/sim/tools/athena/create_named_query.ts b/apps/sim/tools/athena/create_named_query.ts index 8aa9c2aad0b..91b2820fe21 100644 --- a/apps/sim/tools/athena/create_named_query.ts +++ b/apps/sim/tools/athena/create_named_query.ts @@ -11,7 +11,7 @@ export const createNamedQueryTool: ToolConfig< id: 'athena_create_named_query', name: 'Athena Create Named Query', description: 'Create a saved/named query in AWS Athena', - version: '1.0', + version: '1.0.0', params: { awsRegion: { diff --git a/apps/sim/tools/athena/get_named_query.ts b/apps/sim/tools/athena/get_named_query.ts index 28cafac4579..dd57401c458 100644 --- a/apps/sim/tools/athena/get_named_query.ts +++ b/apps/sim/tools/athena/get_named_query.ts @@ -6,7 +6,7 @@ export const getNamedQueryTool: ToolConfig = { rds_delete: rdsDeleteTool, rds_execute: rdsExecuteTool, rds_introspect: rdsIntrospectTool, - cloudformation_describe_stacks: cloudformationDescribeStacksTool, - cloudformation_list_stack_resources: cloudformationListStackResourcesTool, - cloudformation_detect_stack_drift: cloudformationDetectStackDriftTool, cloudformation_describe_stack_drift_detection_status: cloudformationDescribeStackDriftDetectionStatusTool, cloudformation_describe_stack_events: cloudformationDescribeStackEventsTool, + cloudformation_describe_stacks: cloudformationDescribeStacksTool, + cloudformation_detect_stack_drift: cloudformationDetectStackDriftTool, cloudformation_get_template: cloudformationGetTemplateTool, + cloudformation_list_stack_resources: cloudformationListStackResourcesTool, cloudformation_validate_template: cloudformationValidateTemplateTool, - cloudwatch_query_logs: cloudwatchQueryLogsTool, - cloudwatch_describe_log_groups: cloudwatchDescribeLogGroupsTool, cloudwatch_describe_alarms: cloudwatchDescribeAlarmsTool, + cloudwatch_describe_log_groups: cloudwatchDescribeLogGroupsTool, cloudwatch_describe_log_streams: cloudwatchDescribeLogStreamsTool, cloudwatch_get_log_events: cloudwatchGetLogEventsTool, - cloudwatch_list_metrics: cloudwatchListMetricsTool, cloudwatch_get_metric_statistics: cloudwatchGetMetricStatisticsTool, + cloudwatch_list_metrics: cloudwatchListMetricsTool, cloudwatch_put_metric_data: cloudwatchPutMetricDataTool, + cloudwatch_query_logs: cloudwatchQueryLogsTool, dynamodb_get: dynamodbGetTool, dynamodb_put: dynamodbPutTool, dynamodb_query: dynamodbQueryTool, From 5aa53470791378b592c4f2a96eb09c43693b0aeb Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 8 Apr 2026 16:26:45 -0700 Subject: [PATCH 3/7] fix(cloudwatch): complete put_metric_data unit dropdown, add missing outputs, fix JSON error handling - Add all 27 valid CloudWatch StandardUnit values to metricUnit dropdown (was 13) - Add missing block outputs for put_metric_data: success, namespace, metricName, value, unit - Add try-catch around dimensions JSON.parse in put-metric-data route for proper 400 errors Co-Authored-By: Claude Opus 4.6 --- .../tools/cloudwatch/put-metric-data/route.ts | 7 +++- apps/sim/blocks/blocks/cloudwatch.ts | 34 +++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/apps/sim/app/api/tools/cloudwatch/put-metric-data/route.ts b/apps/sim/app/api/tools/cloudwatch/put-metric-data/route.ts index fe0e7473443..50432e512c0 100644 --- a/apps/sim/app/api/tools/cloudwatch/put-metric-data/route.ts +++ b/apps/sim/app/api/tools/cloudwatch/put-metric-data/route.ts @@ -73,7 +73,12 @@ export async function POST(request: NextRequest) { const dimensions: { Name: string; Value: string }[] = [] if (validatedData.dimensions) { - const parsed = JSON.parse(validatedData.dimensions) + let parsed: unknown + try { + parsed = JSON.parse(validatedData.dimensions) + } catch { + return NextResponse.json({ error: 'Invalid dimensions JSON format' }, { status: 400 }) + } if (typeof parsed === 'object' && parsed !== null) { for (const [name, value] of Object.entries(parsed)) { dimensions.push({ Name: name, Value: String(value) }) diff --git a/apps/sim/blocks/blocks/cloudwatch.ts b/apps/sim/blocks/blocks/cloudwatch.ts index 0b6ecfba302..770a21debca 100644 --- a/apps/sim/blocks/blocks/cloudwatch.ts +++ b/apps/sim/blocks/blocks/cloudwatch.ts @@ -271,8 +271,22 @@ Return ONLY the numeric timestamp - no explanations, no quotes, no extra text.`, { label: 'Kilobytes', id: 'Kilobytes' }, { label: 'Megabytes', id: 'Megabytes' }, { label: 'Gigabytes', id: 'Gigabytes' }, + { label: 'Terabytes', id: 'Terabytes' }, { label: 'Bits', id: 'Bits' }, + { label: 'Kilobits', id: 'Kilobits' }, + { label: 'Megabits', id: 'Megabits' }, + { label: 'Gigabits', id: 'Gigabits' }, + { label: 'Terabits', id: 'Terabits' }, { label: 'Bytes/Second', id: 'Bytes/Second' }, + { label: 'Kilobytes/Second', id: 'Kilobytes/Second' }, + { label: 'Megabytes/Second', id: 'Megabytes/Second' }, + { label: 'Gigabytes/Second', id: 'Gigabytes/Second' }, + { label: 'Terabytes/Second', id: 'Terabytes/Second' }, + { label: 'Bits/Second', id: 'Bits/Second' }, + { label: 'Kilobits/Second', id: 'Kilobits/Second' }, + { label: 'Megabits/Second', id: 'Megabits/Second' }, + { label: 'Gigabits/Second', id: 'Gigabits/Second' }, + { label: 'Terabits/Second', id: 'Terabits/Second' }, { label: 'Count/Second', id: 'Count/Second' }, ], value: () => 'None', @@ -678,6 +692,26 @@ Return ONLY the numeric timestamp - no explanations, no quotes, no extra text.`, type: 'array', description: 'CloudWatch alarms with state and configuration', }, + success: { + type: 'boolean', + description: 'Whether the published metric was successful', + }, + namespace: { + type: 'string', + description: 'Metric namespace', + }, + metricName: { + type: 'string', + description: 'Metric name', + }, + value: { + type: 'number', + description: 'Published metric value', + }, + unit: { + type: 'string', + description: 'Metric unit', + }, timestamp: { type: 'string', description: 'Timestamp when metric was published', From 0aecc0f6953e1f05f71c2dc66147bb03b51825e5 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 8 Apr 2026 16:29:51 -0700 Subject: [PATCH 4/7] fix(cloudwatch): fix DescribeAlarms returning only MetricAlarm when "All Types" selected Per AWS docs, omitting AlarmTypes returns only MetricAlarm. Now explicitly sends both MetricAlarm and CompositeAlarm when no filter is selected. Also fix dimensions JSON parse errors returning 500 instead of 400 in get-metric-statistics route. Co-Authored-By: Claude Opus 4.6 --- apps/sim/app/api/tools/cloudwatch/describe-alarms/route.ts | 4 +++- .../app/api/tools/cloudwatch/get-metric-statistics/route.ts | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/sim/app/api/tools/cloudwatch/describe-alarms/route.ts b/apps/sim/app/api/tools/cloudwatch/describe-alarms/route.ts index 3fc65ab5bfd..b4983ee6619 100644 --- a/apps/sim/app/api/tools/cloudwatch/describe-alarms/route.ts +++ b/apps/sim/app/api/tools/cloudwatch/describe-alarms/route.ts @@ -51,7 +51,9 @@ export async function POST(request: NextRequest) { const command = new DescribeAlarmsCommand({ ...(validatedData.alarmNamePrefix && { AlarmNamePrefix: validatedData.alarmNamePrefix }), ...(validatedData.stateValue && { StateValue: validatedData.stateValue as StateValue }), - ...(validatedData.alarmType && { AlarmTypes: [validatedData.alarmType as AlarmType] }), + AlarmTypes: validatedData.alarmType + ? [validatedData.alarmType as AlarmType] + : (['MetricAlarm', 'CompositeAlarm'] as AlarmType[]), ...(validatedData.limit !== undefined && { MaxRecords: validatedData.limit }), }) diff --git a/apps/sim/app/api/tools/cloudwatch/get-metric-statistics/route.ts b/apps/sim/app/api/tools/cloudwatch/get-metric-statistics/route.ts index 677bafca3ca..55d333a6d49 100644 --- a/apps/sim/app/api/tools/cloudwatch/get-metric-statistics/route.ts +++ b/apps/sim/app/api/tools/cloudwatch/get-metric-statistics/route.ts @@ -53,7 +53,7 @@ export async function POST(request: NextRequest) { })) } } catch { - throw new Error('Invalid dimensions JSON') + return NextResponse.json({ error: 'Invalid dimensions JSON format' }, { status: 400 }) } } From 66ffcd9da57b961ba701c3280ae427563b2b5205 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 8 Apr 2026 16:31:27 -0700 Subject: [PATCH 5/7] fix(cloudwatch): validate dimensions JSON at Zod schema level Move dimensions validation from runtime try-catch to Zod refinement, catching malformed JSON and arrays at schema validation time (400) instead of runtime (500). Also rejects JSON arrays that would produce meaningless numeric dimension names. Co-Authored-By: Claude Opus 4.6 --- .../tools/cloudwatch/put-metric-data/route.ts | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/apps/sim/app/api/tools/cloudwatch/put-metric-data/route.ts b/apps/sim/app/api/tools/cloudwatch/put-metric-data/route.ts index 50432e512c0..e2c0dd6f867 100644 --- a/apps/sim/app/api/tools/cloudwatch/put-metric-data/route.ts +++ b/apps/sim/app/api/tools/cloudwatch/put-metric-data/route.ts @@ -48,7 +48,21 @@ const PutMetricDataSchema = z.object({ metricName: z.string().min(1, 'Metric name is required'), value: z.number({ coerce: true }), unit: z.enum(VALID_UNITS).optional(), - dimensions: z.string().optional(), + dimensions: z + .string() + .optional() + .refine( + (val) => { + if (!val) return true + try { + const parsed = JSON.parse(val) + return typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed) + } catch { + return false + } + }, + { message: 'dimensions must be a valid JSON object string' } + ), }) export async function POST(request: NextRequest) { @@ -73,16 +87,9 @@ export async function POST(request: NextRequest) { const dimensions: { Name: string; Value: string }[] = [] if (validatedData.dimensions) { - let parsed: unknown - try { - parsed = JSON.parse(validatedData.dimensions) - } catch { - return NextResponse.json({ error: 'Invalid dimensions JSON format' }, { status: 400 }) - } - if (typeof parsed === 'object' && parsed !== null) { - for (const [name, value] of Object.entries(parsed)) { - dimensions.push({ Name: name, Value: String(value) }) - } + const parsed = JSON.parse(validatedData.dimensions) + for (const [name, value] of Object.entries(parsed)) { + dimensions.push({ Name: name, Value: String(value) }) } } From 1118e4fa8e681cc20ed4b5ec265e94a8103ce6c5 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 8 Apr 2026 17:23:05 -0700 Subject: [PATCH 6/7] fix(cloudwatch): reject non-numeric metricValue instead of silently publishing 0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add NaN guard in block config and .finite() refinement in Zod schema so "abc" → NaN is caught at both layers instead of coercing to 0. Co-Authored-By: Claude Opus 4.6 --- apps/sim/app/api/tools/cloudwatch/put-metric-data/route.ts | 4 +++- apps/sim/blocks/blocks/cloudwatch.ts | 6 +++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/apps/sim/app/api/tools/cloudwatch/put-metric-data/route.ts b/apps/sim/app/api/tools/cloudwatch/put-metric-data/route.ts index e2c0dd6f867..8712b8200c8 100644 --- a/apps/sim/app/api/tools/cloudwatch/put-metric-data/route.ts +++ b/apps/sim/app/api/tools/cloudwatch/put-metric-data/route.ts @@ -46,7 +46,9 @@ const PutMetricDataSchema = z.object({ secretAccessKey: z.string().min(1, 'AWS secret access key is required'), namespace: z.string().min(1, 'Namespace is required'), metricName: z.string().min(1, 'Metric name is required'), - value: z.number({ coerce: true }), + value: z.number({ coerce: true }).refine((v) => Number.isFinite(v), { + message: 'Metric value must be a finite number', + }), unit: z.enum(VALID_UNITS).optional(), dimensions: z .string() diff --git a/apps/sim/blocks/blocks/cloudwatch.ts b/apps/sim/blocks/blocks/cloudwatch.ts index 770a21debca..d811ee95dec 100644 --- a/apps/sim/blocks/blocks/cloudwatch.ts +++ b/apps/sim/blocks/blocks/cloudwatch.ts @@ -570,6 +570,10 @@ Return ONLY the numeric timestamp - no explanations, no quotes, no extra text.`, if (rest.metricValue === undefined || rest.metricValue === '') { throw new Error('Metric value is required') } + const numericValue = Number(rest.metricValue) + if (Number.isNaN(numericValue)) { + throw new Error('Metric value must be a valid number') + } return { awsRegion, @@ -577,7 +581,7 @@ Return ONLY the numeric timestamp - no explanations, no quotes, no extra text.`, awsSecretAccessKey, namespace: rest.metricNamespace, metricName: rest.metricName, - value: Number(rest.metricValue), + value: numericValue, ...(rest.metricUnit && rest.metricUnit !== 'None' && { unit: rest.metricUnit }), ...(rest.publishDimensions && { dimensions: (() => { From 9a4d3945ab2f8f06733a32d1efb2d71ac6c4e652 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 8 Apr 2026 18:37:40 -0700 Subject: [PATCH 7/7] fix(cloudwatch): use Number.isFinite to also reject Infinity in block config Aligns block-level validation with route's Zod .finite() refinement so Infinity/-Infinity are caught at the block config layer, not just the API. Co-Authored-By: Claude Opus 4.6 --- apps/sim/blocks/blocks/cloudwatch.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/sim/blocks/blocks/cloudwatch.ts b/apps/sim/blocks/blocks/cloudwatch.ts index d811ee95dec..30e5245d141 100644 --- a/apps/sim/blocks/blocks/cloudwatch.ts +++ b/apps/sim/blocks/blocks/cloudwatch.ts @@ -571,8 +571,8 @@ Return ONLY the numeric timestamp - no explanations, no quotes, no extra text.`, throw new Error('Metric value is required') } const numericValue = Number(rest.metricValue) - if (Number.isNaN(numericValue)) { - throw new Error('Metric value must be a valid number') + if (!Number.isFinite(numericValue)) { + throw new Error('Metric value must be a finite number') } return {