diff --git a/apps/sim/app/api/workflows/[id]/execute/route.ts b/apps/sim/app/api/workflows/[id]/execute/route.ts index 1520caeec3..0b5db45ccc 100644 --- a/apps/sim/app/api/workflows/[id]/execute/route.ts +++ b/apps/sim/app/api/workflows/[id]/execute/route.ts @@ -197,18 +197,42 @@ async function executeWorkflow(workflow: any, requestId: string, input?: any) { (acc, [blockId, blockState]) => { // Check if this block has a responseFormat that needs to be parsed if (blockState.responseFormat && typeof blockState.responseFormat === 'string') { - try { - logger.debug(`[${requestId}] Parsing responseFormat for block ${blockId}`) - // Attempt to parse the responseFormat if it's a string - const parsedResponseFormat = JSON.parse(blockState.responseFormat) - + const responseFormatValue = blockState.responseFormat.trim() + + // Check for variable references like + if (responseFormatValue.startsWith('<') && responseFormatValue.includes('>')) { + logger.debug( + `[${requestId}] Response format contains variable reference for block ${blockId}` + ) + // Keep variable references as-is - they will be resolved during execution + acc[blockId] = blockState + } else if (responseFormatValue === '') { + // Empty string - remove response format acc[blockId] = { ...blockState, - responseFormat: parsedResponseFormat, + responseFormat: undefined, + } + } else { + try { + logger.debug(`[${requestId}] Parsing responseFormat for block ${blockId}`) + // Attempt to parse the responseFormat if it's a string + const parsedResponseFormat = JSON.parse(responseFormatValue) + + acc[blockId] = { + ...blockState, + responseFormat: parsedResponseFormat, + } + } catch (error) { + logger.warn( + `[${requestId}] Failed to parse responseFormat for block ${blockId}, using undefined`, + error + ) + // Set to undefined instead of keeping malformed JSON - this allows execution to continue + acc[blockId] = { + ...blockState, + responseFormat: undefined, + } } - } catch (error) { - logger.warn(`[${requestId}] Failed to parse responseFormat for block ${blockId}`, error) - acc[blockId] = blockState } } else { acc[blockId] = blockState diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/response/response-format.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/response/response-format.tsx index edef012ccc..9f6a62c2e2 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/response/response-format.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/response/response-format.tsx @@ -290,7 +290,13 @@ export function ResponseFormat({ {showPreview && (
-            {JSON.stringify(generateJSON(properties), null, 2)}
+            {(() => {
+              try {
+                return JSON.stringify(generateJSON(properties), null, 2)
+              } catch (error) {
+                return `Error generating preview: ${error instanceof Error ? error.message : 'Unknown error'}`
+              }
+            })()}
           
)} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-connections.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-connections.ts index fca7b12870..898e5c7ea5 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-connections.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-connections.ts @@ -29,6 +29,35 @@ export interface ConnectedBlock { } } +function parseResponseFormatSafely(responseFormatValue: any, blockId: string): any { + if (!responseFormatValue) { + return undefined + } + + if (typeof responseFormatValue === 'object' && responseFormatValue !== null) { + return responseFormatValue + } + + if (typeof responseFormatValue === 'string') { + const trimmedValue = responseFormatValue.trim() + + if (trimmedValue.startsWith('<') && trimmedValue.includes('>')) { + return trimmedValue + } + + if (trimmedValue === '') { + return undefined + } + + try { + return JSON.parse(trimmedValue) + } catch (error) { + return undefined + } + } + return undefined +} + // Helper function to extract fields from JSON Schema function extractFieldsFromSchema(schema: any): Field[] { if (!schema || typeof schema !== 'object') { @@ -77,15 +106,8 @@ export function useBlockConnections(blockId: string) { let responseFormat - try { - responseFormat = - typeof responseFormatValue === 'string' && responseFormatValue - ? JSON.parse(responseFormatValue) - : responseFormatValue // Handle case where it's already an object - } catch (e) { - logger.error('Failed to parse response format:', { e }) - responseFormat = undefined - } + // Safely parse response format with proper error handling + responseFormat = parseResponseFormatSafely(responseFormatValue, sourceId) // Get the default output type from the block's outputs const defaultOutputs: Field[] = Object.entries(sourceBlock.outputs || {}).map(([key]) => ({ @@ -120,15 +142,8 @@ export function useBlockConnections(blockId: string) { let responseFormat - try { - responseFormat = - typeof responseFormatValue === 'string' && responseFormatValue - ? JSON.parse(responseFormatValue) - : responseFormatValue // Handle case where it's already an object - } catch (e) { - logger.error('Failed to parse response format:', { e }) - responseFormat = undefined - } + // Safely parse response format with proper error handling + responseFormat = parseResponseFormatSafely(responseFormatValue, edge.source) // Get the default output type from the block's outputs const defaultOutputs: Field[] = Object.entries(sourceBlock.outputs || {}).map(([key]) => ({ diff --git a/apps/sim/executor/handlers/agent/agent-handler.test.ts b/apps/sim/executor/handlers/agent/agent-handler.test.ts index b2e0235b87..b6933907ec 100644 --- a/apps/sim/executor/handlers/agent/agent-handler.test.ts +++ b/apps/sim/executor/handlers/agent/agent-handler.test.ts @@ -736,7 +736,29 @@ describe('AgentBlockHandler', () => { }) }) - it('should throw an error for invalid JSON in responseFormat', async () => { + it('should handle invalid JSON in responseFormat gracefully', async () => { + mockFetch.mockImplementationOnce(() => { + return Promise.resolve({ + ok: true, + headers: { + get: (name: string) => { + if (name === 'Content-Type') return 'application/json' + if (name === 'X-Execution-Data') return null + return null + }, + }, + json: () => + Promise.resolve({ + content: 'Regular text response', + model: 'mock-model', + tokens: { prompt: 10, completion: 20, total: 30 }, + timing: { total: 100 }, + toolCalls: [], + cost: undefined, + }), + }) + }) + const inputs = { model: 'gpt-4o', userPrompt: 'Format this output.', @@ -744,9 +766,60 @@ describe('AgentBlockHandler', () => { responseFormat: '{invalid-json', } - await expect(handler.execute(mockBlock, inputs, mockContext)).rejects.toThrow( - 'Invalid response' - ) + // Should not throw an error, but continue with default behavior + const result = await handler.execute(mockBlock, inputs, mockContext) + + expect(result).toEqual({ + content: 'Regular text response', + model: 'mock-model', + tokens: { prompt: 10, completion: 20, total: 30 }, + toolCalls: { list: [], count: 0 }, + providerTiming: { total: 100 }, + cost: undefined, + }) + }) + + it('should handle variable references in responseFormat gracefully', async () => { + mockFetch.mockImplementationOnce(() => { + return Promise.resolve({ + ok: true, + headers: { + get: (name: string) => { + if (name === 'Content-Type') return 'application/json' + if (name === 'X-Execution-Data') return null + return null + }, + }, + json: () => + Promise.resolve({ + content: 'Regular text response', + model: 'mock-model', + tokens: { prompt: 10, completion: 20, total: 30 }, + timing: { total: 100 }, + toolCalls: [], + cost: undefined, + }), + }) + }) + + const inputs = { + model: 'gpt-4o', + userPrompt: 'Format this output.', + apiKey: 'test-api-key', + responseFormat: '', + } + + // Should not throw an error, but continue with default behavior + const result = await handler.execute(mockBlock, inputs, mockContext) + + expect(result).toEqual({ + content: 'Regular text response', + model: 'mock-model', + tokens: { prompt: 10, completion: 20, total: 30 }, + toolCalls: { list: [], count: 0 }, + providerTiming: { total: 100 }, + cost: undefined, + }) }) it('should handle errors from the provider request', async () => { diff --git a/apps/sim/executor/handlers/agent/agent-handler.ts b/apps/sim/executor/handlers/agent/agent-handler.ts index aeb2fae04d..80382f5fff 100644 --- a/apps/sim/executor/handlers/agent/agent-handler.ts +++ b/apps/sim/executor/handlers/agent/agent-handler.ts @@ -58,22 +58,63 @@ export class AgentBlockHandler implements BlockHandler { private parseResponseFormat(responseFormat?: string | object): any { if (!responseFormat || responseFormat === '') return undefined - try { - const parsed = - typeof responseFormat === 'string' ? JSON.parse(responseFormat) : responseFormat - - if (parsed && typeof parsed === 'object' && !parsed.schema && !parsed.name) { + // If already an object, process it directly + if (typeof responseFormat === 'object' && responseFormat !== null) { + const formatObj = responseFormat as any + if (!formatObj.schema && !formatObj.name) { return { name: 'response_schema', - schema: parsed, + schema: responseFormat, strict: true, } } - return parsed - } catch (error: any) { - logger.error('Failed to parse response format:', { error }) - throw new Error(`Invalid response format: ${error.message}`) + return responseFormat + } + + // Handle string values + if (typeof responseFormat === 'string') { + const trimmedValue = responseFormat.trim() + + // Check for variable references like + if (trimmedValue.startsWith('<') && trimmedValue.includes('>')) { + logger.info('Response format contains variable reference:', { + value: trimmedValue, + }) + // Variable references should have been resolved by the resolver before reaching here + // If we still have a variable reference, it means it couldn't be resolved + // Return undefined to use default behavior (no structured response) + return undefined + } + + // Try to parse as JSON + try { + const parsed = JSON.parse(trimmedValue) + + if (parsed && typeof parsed === 'object' && !parsed.schema && !parsed.name) { + return { + name: 'response_schema', + schema: parsed, + strict: true, + } + } + return parsed + } catch (error: any) { + logger.warn('Failed to parse response format as JSON, using default behavior:', { + error: error.message, + value: trimmedValue, + }) + // Return undefined instead of throwing - this allows execution to continue + // without structured response format + return undefined + } } + + // For any other type, return undefined + logger.warn('Unexpected response format type, using default behavior:', { + type: typeof responseFormat, + value: responseFormat, + }) + return undefined } private async formatTools(inputTools: ToolInput[], context: ExecutionContext): Promise { diff --git a/apps/sim/serializer/index.ts b/apps/sim/serializer/index.ts index 955264c706..ef02cf2728 100644 --- a/apps/sim/serializer/index.ts +++ b/apps/sim/serializer/index.ts @@ -121,7 +121,7 @@ export class Serializer { // Include response format fields if available ...(params.responseFormat ? { - responseFormat: JSON.parse(params.responseFormat), + responseFormat: this.parseResponseFormatSafely(params.responseFormat), } : {}), }, @@ -136,6 +136,48 @@ export class Serializer { } } + private parseResponseFormatSafely(responseFormat: any): any { + if (!responseFormat) { + return undefined + } + + // If already an object, return as-is + if (typeof responseFormat === 'object' && responseFormat !== null) { + return responseFormat + } + + // Handle string values + if (typeof responseFormat === 'string') { + const trimmedValue = responseFormat.trim() + + // Check for variable references like + if (trimmedValue.startsWith('<') && trimmedValue.includes('>')) { + // Keep variable references as-is + return trimmedValue + } + + if (trimmedValue === '') { + return undefined + } + + // Try to parse as JSON + try { + return JSON.parse(trimmedValue) + } catch (error) { + // If parsing fails, return undefined to avoid crashes + // This allows the workflow to continue without structured response format + logger.warn('Failed to parse response format as JSON in serializer, using undefined:', { + value: trimmedValue, + error: error instanceof Error ? error.message : String(error), + }) + return undefined + } + } + + // For any other type, return undefined + return undefined + } + private extractParams(block: BlockState): Record { // Special handling for subflow blocks (loops, parallels, etc.) if (block.type === 'loop' || block.type === 'parallel') {