diff --git a/apps/sim/tools/workflow/executor.test.ts b/apps/sim/tools/workflow/executor.test.ts index 14b42619049..764a6f95d59 100644 --- a/apps/sim/tools/workflow/executor.test.ts +++ b/apps/sim/tools/workflow/executor.test.ts @@ -242,6 +242,131 @@ describe('workflowExecutorTool', () => { }) }) + describe('transformResponse', () => { + const transformResponse = workflowExecutorTool.transformResponse! + + function mockResponse(body: any, status = 200): Response { + return { + ok: status >= 200 && status < 300, + status, + json: async () => body, + } as unknown as Response + } + + it.concurrent('should parse standard format response', async () => { + const body = { + success: true, + executionId: '550e8400-e29b-41d4-a716-446655440000', + output: { result: 'hello' }, + metadata: { duration: 500 }, + } + + const result = await transformResponse(mockResponse(body)) + + expect(result.success).toBe(true) + expect(result.output).toEqual({ result: 'hello' }) + expect(result.duration).toBe(500) + expect(result.error).toBeUndefined() + }) + + it.concurrent('should parse standard format failure', async () => { + const body = { + success: false, + executionId: '550e8400-e29b-41d4-a716-446655440000', + output: {}, + error: 'Something went wrong', + } + + const result = await transformResponse(mockResponse(body)) + + expect(result.success).toBe(false) + expect(result.error).toBe('Something went wrong') + }) + + it.concurrent( + 'should handle Response block format (no success/executionId wrapper)', + async () => { + const body = { issues: [], total: 0 } + + const result = await transformResponse(mockResponse(body)) + + expect(result.success).toBe(true) + expect(result.output).toEqual({ issues: [], total: 0 }) + expect(result.error).toBeUndefined() + } + ) + + it.concurrent( + 'should not misidentify user data containing success field as standard format', + async () => { + const body = { success: true, data: [1, 2, 3] } + + const result = await transformResponse(mockResponse(body)) + + // No executionId → treated as Response block format + expect(result.success).toBe(true) + expect(result.output).toEqual({ success: true, data: [1, 2, 3] }) + } + ) + + it.concurrent('should handle empty Response block data', async () => { + const body = {} + + const result = await transformResponse(mockResponse(body)) + + expect(result.success).toBe(true) + expect(result.output).toEqual({}) + }) + + it.concurrent('should handle array Response block data', async () => { + const body = [1, 2, 3] + + const result = await transformResponse(mockResponse(body)) + + expect(result.success).toBe(true) + expect(result.output).toEqual([1, 2, 3]) + }) + + it.concurrent('should preserve error field from Response block data', async () => { + const body = { results: [], error: 'No results found' } + + const result = await transformResponse(mockResponse(body)) + + expect(result.success).toBe(true) + expect(result.output).toEqual({ results: [], error: 'No results found' }) + expect(result.error).toBe('No results found') + }) + + it.concurrent( + 'should not misidentify user data with success + non-UUID executionId as standard format', + async () => { + const body = { success: true, executionId: 'my-exec-run', data: [1, 2, 3] } + + const result = await transformResponse(mockResponse(body)) + + expect(result.success).toBe(true) + expect(result.output).toEqual({ + success: true, + executionId: 'my-exec-run', + data: [1, 2, 3], + }) + } + ) + + it.concurrent( + 'should not leak user payload fields into childWorkflowId/childWorkflowName', + async () => { + const body = { workflowId: 'user-wf-id', workflowName: 'User WF', data: 'test' } + + const result = await transformResponse(mockResponse(body)) + + expect(result.childWorkflowId).toBe('') + expect(result.childWorkflowName).toBe('') + expect(result.output).toEqual(body) + } + ) + }) + describe('tool metadata', () => { it.concurrent('should have correct id', () => { expect(workflowExecutorTool.id).toBe('workflow_executor') diff --git a/apps/sim/tools/workflow/executor.ts b/apps/sim/tools/workflow/executor.ts index 44ebff2b5b1..c20c082ef5c 100644 --- a/apps/sim/tools/workflow/executor.ts +++ b/apps/sim/tools/workflow/executor.ts @@ -1,6 +1,8 @@ import type { ToolConfig } from '@/tools/types' import type { WorkflowExecutorParams, WorkflowExecutorResponse } from '@/tools/workflow/types' +const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i + /** * Tool for executing workflows as blocks within other workflows. * This tool is used by the WorkflowBlockHandler to provide the execution capability. @@ -53,15 +55,26 @@ export const workflowExecutorTool: ToolConfig< }, transformResponse: async (response: Response) => { const data = await response.json() - const outputData = data?.output ?? {} + + // The execute endpoint has two response shapes: + // 1. Standard: { success, executionId, output, error, metadata } + // 2. Response block: arbitrary user-defined data (no wrapper) + // Detect standard format via executionId (always a UUID from uuidv4()) + success boolean. + const isStandardFormat = + typeof data?.success === 'boolean' && + typeof data?.executionId === 'string' && + UUID_RE.test(data.executionId) + + const outputData = isStandardFormat ? (data.output ?? {}) : data + const success = isStandardFormat ? data.success : response.ok return { - success: data?.success ?? false, - duration: data?.metadata?.duration ?? 0, - childWorkflowId: data?.workflowId ?? '', - childWorkflowName: data?.workflowName ?? '', - output: outputData, // For OpenAI provider - result: outputData, // For backwards compatibility + success, + duration: isStandardFormat ? (data?.metadata?.duration ?? 0) : 0, + childWorkflowId: isStandardFormat ? (data?.workflowId ?? '') : '', + childWorkflowName: isStandardFormat ? (data?.workflowName ?? '') : '', + output: outputData, + result: outputData, error: data?.error, } },