From a8504f7116b66a256f033c891d2cc0c0304bd691 Mon Sep 17 00:00:00 2001 From: waleed Date: Wed, 11 Mar 2026 23:56:07 -0700 Subject: [PATCH 1/2] fix(executor): handle Response block format in workflow executor tool When a child workflow has a Response block, the execute endpoint returns the block's data directly without the standard {success, executionId, output} wrapper. The transformResponse was defaulting success to false, causing "Tool execution failed" errors even when the workflow completed successfully. Detect the response format by checking for executionId + success boolean, and fall back to response.ok for Response block responses. Co-Authored-By: Claude Opus 4.6 --- apps/sim/tools/workflow/executor.test.ts | 96 ++++++++++++++++++++++++ apps/sim/tools/workflow/executor.ts | 19 +++-- 2 files changed, 110 insertions(+), 5 deletions(-) diff --git a/apps/sim/tools/workflow/executor.test.ts b/apps/sim/tools/workflow/executor.test.ts index 14b42619049..82929127c2c 100644 --- a/apps/sim/tools/workflow/executor.test.ts +++ b/apps/sim/tools/workflow/executor.test.ts @@ -242,6 +242,102 @@ 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: 'exec-123', + 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: 'exec-123', + 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') + }) + }) + 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..93d3d3e69ac 100644 --- a/apps/sim/tools/workflow/executor.ts +++ b/apps/sim/tools/workflow/executor.ts @@ -53,15 +53,24 @@ 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 by checking for executionId (always present) + success boolean. + const isStandardFormat = + typeof data?.success === 'boolean' && typeof data?.executionId === 'string' + + const outputData = isStandardFormat ? (data.output ?? {}) : data + const success = isStandardFormat ? data.success : response.ok return { - success: data?.success ?? false, - duration: data?.metadata?.duration ?? 0, + success, + duration: isStandardFormat ? (data?.metadata?.duration ?? 0) : 0, childWorkflowId: data?.workflowId ?? '', childWorkflowName: data?.workflowName ?? '', - output: outputData, // For OpenAI provider - result: outputData, // For backwards compatibility + output: outputData, + result: outputData, error: data?.error, } }, From 135be67e1435c07a284a0e27ed800914ae63a12d Mon Sep 17 00:00:00 2001 From: waleed Date: Thu, 12 Mar 2026 00:16:13 -0700 Subject: [PATCH 2/2] fix(executor): strengthen format detection per PR review - Add UUID validation on executionId to eliminate false-positive detection when user Response block data coincidentally contains success + executionId - Scope childWorkflowId/childWorkflowName to standard format only so user payload fields don't leak into child workflow metadata - Add test coverage for both edge cases Co-Authored-By: Claude Opus 4.6 --- apps/sim/tools/workflow/executor.test.ts | 33 ++++++++++++++++++++++-- apps/sim/tools/workflow/executor.ts | 12 ++++++--- 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/apps/sim/tools/workflow/executor.test.ts b/apps/sim/tools/workflow/executor.test.ts index 82929127c2c..764a6f95d59 100644 --- a/apps/sim/tools/workflow/executor.test.ts +++ b/apps/sim/tools/workflow/executor.test.ts @@ -256,7 +256,7 @@ describe('workflowExecutorTool', () => { it.concurrent('should parse standard format response', async () => { const body = { success: true, - executionId: 'exec-123', + executionId: '550e8400-e29b-41d4-a716-446655440000', output: { result: 'hello' }, metadata: { duration: 500 }, } @@ -272,7 +272,7 @@ describe('workflowExecutorTool', () => { it.concurrent('should parse standard format failure', async () => { const body = { success: false, - executionId: 'exec-123', + executionId: '550e8400-e29b-41d4-a716-446655440000', output: {}, error: 'Something went wrong', } @@ -336,6 +336,35 @@ describe('workflowExecutorTool', () => { 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', () => { diff --git a/apps/sim/tools/workflow/executor.ts b/apps/sim/tools/workflow/executor.ts index 93d3d3e69ac..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. @@ -57,9 +59,11 @@ export const workflowExecutorTool: ToolConfig< // 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 by checking for executionId (always present) + success boolean. + // Detect standard format via executionId (always a UUID from uuidv4()) + success boolean. const isStandardFormat = - typeof data?.success === 'boolean' && typeof data?.executionId === 'string' + 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 @@ -67,8 +71,8 @@ export const workflowExecutorTool: ToolConfig< return { success, duration: isStandardFormat ? (data?.metadata?.duration ?? 0) : 0, - childWorkflowId: data?.workflowId ?? '', - childWorkflowName: data?.workflowName ?? '', + childWorkflowId: isStandardFormat ? (data?.workflowId ?? '') : '', + childWorkflowName: isStandardFormat ? (data?.workflowName ?? '') : '', output: outputData, result: outputData, error: data?.error,