Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 125 additions & 0 deletions apps/sim/tools/workflow/executor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
27 changes: 20 additions & 7 deletions apps/sim/tools/workflow/executor.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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,
}
},
Expand Down
Loading