diff --git a/sim/app/api/workflows/[id]/execute/route.ts b/sim/app/api/workflows/[id]/execute/route.ts index 88e8d8008e..627c51d59f 100644 --- a/sim/app/api/workflows/[id]/execute/route.ts +++ b/sim/app/api/workflows/[id]/execute/route.ts @@ -37,6 +37,21 @@ async function executeWorkflow(workflow: any, requestId: string, input?: any) { throw new Error('Workflow is already running') } + // Log input to help debug + logger.info(`[${requestId}] Executing workflow with input:`, + input ? JSON.stringify(input, null, 2) : 'No input provided'); + + // Validate and structure input for maximum compatibility + let processedInput = input; + if (input && typeof input === 'object') { + // Ensure input is properly structured for the starter block + if (input.input === undefined) { + // If input is not already nested, structure it properly + processedInput = { input: input }; + logger.info(`[${requestId}] Restructured input for workflow:`, JSON.stringify(processedInput, null, 2)); + } + } + try { runningExecutions.add(workflowId) logger.info(`[${requestId}] Starting workflow execution: ${workflowId}`) @@ -176,7 +191,7 @@ async function executeWorkflow(workflow: any, requestId: string, input?: any) { serializedWorkflow, processedBlockStates, decryptedEnvVars, - input, + processedInput, workflowVariables ) @@ -262,9 +277,29 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ } const bodyText = await request.text() - const body = bodyText ? JSON.parse(bodyText) : {} + logger.info(`[${requestId}] Raw request body:`, bodyText) + + let body = {} + if (bodyText && bodyText.trim()) { + try { + body = JSON.parse(bodyText) + logger.info(`[${requestId}] Parsed request body:`, JSON.stringify(body, null, 2)) + } catch (error) { + logger.error(`[${requestId}] Failed to parse request body:`, error) + return createErrorResponse('Invalid JSON in request body', 400, 'INVALID_JSON') + } + } else { + logger.info(`[${requestId}] No request body provided`) + } - const result = await executeWorkflow(validation.workflow, requestId, body) + // Don't double-nest the input if it's already structured + const hasContent = Object.keys(body).length > 0; + const input = hasContent ? { input: body } : {}; + + logger.info(`[${requestId}] Input passed to workflow:`, JSON.stringify(input, null, 2)) + + // Execute workflow with the structured input + const result = await executeWorkflow(validation.workflow, requestId, input) return createSuccessResponse(result) } catch (error: any) { logger.error(`[${requestId}] Error executing workflow: ${id}`, error) diff --git a/sim/executor/handlers.ts b/sim/executor/handlers.ts index a468381def..2670c37190 100644 --- a/sim/executor/handlers.ts +++ b/sim/executor/handlers.ts @@ -819,8 +819,34 @@ export class ApiBlockHandler implements BlockHandler { } try { + let processedInputs = { ...inputs }; + + // Handle body specifically to ensure it's properly processed for API requests + if (processedInputs.body !== undefined) { + // If body is a string that looks like JSON, parse it + if (typeof processedInputs.body === 'string') { + try { + // Trim whitespace before checking for JSON pattern + const trimmedBody = processedInputs.body.trim(); + if (trimmedBody.startsWith('{') || trimmedBody.startsWith('[')) { + processedInputs.body = JSON.parse(trimmedBody); + logger.info('[ApiBlockHandler] Parsed JSON body:', JSON.stringify(processedInputs.body, null, 2)); + } + } catch (e) { + logger.info('[ApiBlockHandler] Failed to parse body as JSON, using as string:', e); + // Keep as string if parsing fails + } + } else if (processedInputs.body === null) { + // Convert null to undefined for consistency with API expectations + processedInputs.body = undefined; + } + } + + // Ensure the final processed body is logged + logger.info('[ApiBlockHandler] Final processed request body:', JSON.stringify(processedInputs.body, null, 2)); + const result = await executeTool(block.config.tool, { - ...inputs, + ...processedInputs, _context: { workflowId: context.workflowId }, }) @@ -932,7 +958,6 @@ export class FunctionBlockHandler implements BlockHandler { inputs: Record, context: ExecutionContext ): Promise { - // Prepare code for execution const codeContent = Array.isArray(inputs.code) ? inputs.code.map((c: { content: string }) => c.content).join('\n') : inputs.code diff --git a/sim/executor/index.ts b/sim/executor/index.ts index c17573c0ff..6c1c1b728c 100644 --- a/sim/executor/index.ts +++ b/sim/executor/index.ts @@ -43,8 +43,14 @@ export class Executor { private workflowVariables: Record = {} ) { this.validateWorkflow() - this.workflowInput = workflowInput || {} + if (workflowInput) { + this.workflowInput = workflowInput; + logger.info('[Executor] Using workflow input:', JSON.stringify(this.workflowInput, null, 2)); + } else { + this.workflowInput = {}; + } + this.loopManager = new LoopManager(workflow.loops || {}) this.resolver = new InputResolver( workflow, @@ -341,6 +347,7 @@ export class Executor { // Initialize the starter block with the workflow input try { const blockParams = starterBlock.config.params + /* Commenting out input format handling const inputFormat = blockParams?.inputFormat // If input format is defined, structure the input according to the schema @@ -352,7 +359,14 @@ export class Executor { for (const field of inputFormat) { if (field.name && field.type) { // Get the field value from workflow input if available - const inputValue = this.workflowInput?.[field.name] + // First try to access via input.field, then directly from field + // This handles both input formats: { input: { field: value } } and { field: value } + const inputValue = this.workflowInput?.input?.[field.name] !== undefined + ? this.workflowInput.input[field.name] // Try to get from input.field + : this.workflowInput?.[field.name]; // Fallback to direct field access + + logger.info(`[Executor] Processing input field ${field.name} (${field.type}):`, + inputValue !== undefined ? JSON.stringify(inputValue) : 'undefined'); // Convert the value to the appropriate type let typedValue = inputValue @@ -378,13 +392,27 @@ export class Executor { } } + // Check if we managed to process any fields - if not, use the raw input + const hasProcessedFields = Object.keys(structuredInput).length > 0; + + // If no fields matched the input format, extract the raw input to use instead + const rawInputData = this.workflowInput?.input !== undefined + ? this.workflowInput.input // Use the nested input data + : this.workflowInput; // Fallback to direct input + + // Use the structured input if we processed fields, otherwise use raw input + const finalInput = hasProcessedFields ? structuredInput : rawInputData; + // Initialize the starter block with structured input + // Ensure both input and direct fields are available const starterOutput = { response: { - input: structuredInput, - ...structuredInput, // Add input fields directly at response level too + input: finalInput, + ...finalInput, // Add input fields directly at response level too }, } + + logger.info(`[Executor] Starter output:`, JSON.stringify(starterOutput, null, 2)); context.blockStates.set(starterBlock.id, { output: starterOutput, @@ -392,23 +420,31 @@ export class Executor { executionTime: 0, }) } else { + */ // No input format defined or not an array, - // check if we're receiving input from API call + // Handle API call - prioritize using the input as-is if (this.workflowInput && typeof this.workflowInput === 'object') { - // For API calls, use the raw input but make it accessible at both paths + // For API calls, extract input from the nested structure if it exists + const inputData = this.workflowInput.input !== undefined + ? this.workflowInput.input // Use the nested input data + : this.workflowInput; // Fallback to direct input + + // Create starter output with both formats for maximum compatibility const starterOutput = { response: { - input: this.workflowInput, - ...this.workflowInput, // Make fields directly accessible at response level + input: inputData, + ...inputData, // Make fields directly accessible at response level }, } - logger.info(`Using API input type: ${typeof this.workflowInput}`, { - isArray: Array.isArray(this.workflowInput), - keys: Object.keys(this.workflowInput), - rawInput: JSON.stringify(this.workflowInput), - inputEmpty: Object.keys(this.workflowInput).length === 0, - }) + logger.info(`[Executor] API input for starter block:`, { + type: typeof inputData, + isArray: Array.isArray(inputData), + keys: Object.keys(inputData), + inputData: JSON.stringify(inputData, null, 2), + }); + + logger.info(`[Executor] Final starter block output:`, JSON.stringify(starterOutput, null, 2)); context.blockStates.set(starterBlock.id, { output: starterOutput, @@ -422,6 +458,8 @@ export class Executor { input: this.workflowInput, }, } + + logger.info(`[Executor] Simple starter output:`, JSON.stringify(starterOutput, null, 2)); context.blockStates.set(starterBlock.id, { output: starterOutput, @@ -429,16 +467,24 @@ export class Executor { executionTime: 0, }) } - } + //} // End of inputFormat conditional } catch (e) { logger.warn('Error processing starter block input format:', e) + // Fallback to raw input with both paths accessible + // Ensure we handle both input formats + const inputData = this.workflowInput.input !== undefined + ? this.workflowInput.input // Use nested input if available + : this.workflowInput; // Fallback to direct input + const starterOutput = { response: { - input: this.workflowInput, - ...this.workflowInput, // Add input fields directly at response level too + input: inputData, + ...inputData, // Add input fields directly at response level too }, } + + logger.info(`[Executor] Fallback starter output:`, JSON.stringify(starterOutput, null, 2)); context.blockStates.set(starterBlock.id, { output: starterOutput, @@ -446,10 +492,12 @@ export class Executor { executionTime: 0, }) } - - // Mark the starter block as executed and add its connections to the active path + // Ensure the starter block is in the active execution path + context.activeExecutionPath.add(starterBlock.id); + // Mark the starter block as executed context.executedBlocks.add(starterBlock.id) + // Add all blocks connected to the starter to the active execution path const connectedToStarter = this.workflow.connections .filter((conn) => conn.source === starterBlock.id) .map((conn) => conn.target) @@ -634,25 +682,46 @@ export class Executor { throw new Error(`Cannot execute disabled block: ${block.metadata?.name || block.id}`) } - const inputs = this.resolver.resolveInputs(block, context) + // Check if this block needs the starter block's output + // This is especially relevant for API, function, and conditions that might reference + const starterBlock = this.workflow.blocks.find((b) => b.metadata?.id === 'starter') + if (starterBlock) { + const starterState = context.blockStates.get(starterBlock.id) + if (!starterState) { + logger.warn(`Starter block state not found when executing ${block.metadata?.name || blockId}. This may cause reference errors.`) + } else { + logger.debug(`Starter block state available for ${block.metadata?.name || blockId}:`, + JSON.stringify(starterState.output || {}, null, 2)); + } + } + // Resolve inputs (which will look up references to other blocks including starter) + const inputs = this.resolver.resolveInputs(block, context) + logger.debug(`Resolved inputs for ${block.metadata?.name || blockId}:`, + JSON.stringify(inputs, null, 2)); + + // Find the appropriate handler const handler = this.blockHandlers.find((h) => h.canHandle(block)) if (!handler) { throw new Error(`No handler found for block type: ${block.metadata?.id}`) } + // Execute the block const startTime = performance.now() const rawOutput = await handler.execute(block, inputs, context) const executionTime = performance.now() - startTime + // Normalize the output const output = this.normalizeBlockOutput(rawOutput, block) + // Update the context with the execution result context.blockStates.set(blockId, { output, executed: true, executionTime, }) + // Update the execution log blockLog.success = true blockLog.output = output blockLog.durationMs = Math.round(executionTime) diff --git a/sim/executor/resolver.ts b/sim/executor/resolver.ts index 62e6c0850d..676b95a0c1 100644 --- a/sim/executor/resolver.ts +++ b/sim/executor/resolver.ts @@ -1,6 +1,9 @@ import { SerializedBlock, SerializedWorkflow } from '@/serializer/types' import { LoopManager } from './loops' import { ExecutionContext } from './types' +import { createLogger } from '@/lib/logs/console-logger' + +const logger = createLogger('InputResolver') /** * Resolves input values for blocks by handling references and variable substitution. @@ -51,8 +54,6 @@ export class InputResolver { resolveInputs(block: SerializedBlock, context: ExecutionContext): Record { const inputs = { ...block.config.params } const result: Record = {} - const isFunctionBlock = block.metadata?.id === 'function' - // Process each input parameter for (const [key, value] of Object.entries(inputs)) { // Skip null or undefined values @@ -78,16 +79,39 @@ export class InputResolver { // Resolve environment variables resolvedValue = this.resolveEnvVariables(resolvedValue, isApiKey) + // Special handling for different block types + const isFunctionBlock = block.metadata?.id === 'function' + const isApiBlock = block.metadata?.id === 'api' + // For function blocks, we need special handling for code input if (isFunctionBlock && key === 'code') { // For code input in function blocks, we don't want to parse JSON result[key] = resolvedValue + logger.debug(`[resolveInputs] Function block code input preserved as string`); } + // For API blocks, handle body input specially + else if (isApiBlock && key === 'body') { + try { + // If it's JSON-looking, preserve its structure + if (resolvedValue.trim().startsWith('{') || resolvedValue.trim().startsWith('[')) { + result[key] = JSON.parse(resolvedValue) + logger.debug(`[resolveInputs] API block body parsed as JSON object`); + } else { + result[key] = resolvedValue + logger.debug(`[resolveInputs] API block body preserved as string`); + } + } catch { + // If parsing fails, keep as string + result[key] = resolvedValue + logger.debug(`[resolveInputs] API block body JSON parsing failed, keeping as string`); + } + } // For other inputs, try to convert JSON strings to objects else { try { if (resolvedValue.startsWith('{') || resolvedValue.startsWith('[')) { result[key] = JSON.parse(resolvedValue) + logger.debug(`[resolveInputs] Parsed JSON value for ${key}`); } else { result[key] = resolvedValue } @@ -272,6 +296,101 @@ export class InputResolver { const path = match.slice(1, -1) const [blockRef, ...pathParts] = path.split('.') + // Log the reference being processed + logger.debug(`[resolveBlockReferences] Processing block reference: ${match}`, { + blockRef, + pathParts, + currentBlock: currentBlock.id, + currentBlockType: currentBlock.metadata?.id + }); + + // Special case for "start" references + // This allows users to reference the starter block using + // regardless of the actual name of the starter block + if (blockRef.toLowerCase() === 'start') { + // Find the starter block + const starterBlock = this.workflow.blocks.find((block) => block.metadata?.id === 'starter') + if (starterBlock) { + logger.debug(`[resolveBlockReferences] Found starter block with ID: ${starterBlock.id}`); + + const blockState = context.blockStates.get(starterBlock.id) + if (blockState) { + logger.debug(`[resolveBlockReferences] Starter block state:`, JSON.stringify(blockState, null, 2)); + + // Navigate through the path parts + let replacementValue: any = blockState.output + + // Log the initial output value from the starter block + logger.debug(`[resolveBlockReferences] Initial starter output:`, JSON.stringify(replacementValue, null, 2)); + + for (const part of pathParts) { + logger.debug(`[resolveBlockReferences] Navigating path part: ${part}`, { + currentValue: typeof replacementValue === 'object' ? + JSON.stringify(replacementValue) : replacementValue + }); + + if (!replacementValue || typeof replacementValue !== 'object') { + logger.warn(`[resolveBlockReferences] Invalid path "${part}" - replacementValue is not an object:`, replacementValue); + throw new Error(`Invalid path "${part}" in "${path}" for starter block.`) + } + + replacementValue = replacementValue[part] + + if (replacementValue === undefined) { + logger.warn(`[resolveBlockReferences] No value found at path "${part}" in starter block.`); + throw new Error(`No value found at path "${path}" in starter block.`) + } + } + + // Format the value based on block type and path + let formattedValue: string; + + // Special handling for all blocks referencing starter input + if (blockRef.toLowerCase() === 'start' && pathParts.join('.').includes('input')) { + const blockType = currentBlock.metadata?.id; + + // Format based on which block is consuming this value + if (typeof replacementValue === 'object' && replacementValue !== null) { + // For function blocks, preserve the object structure for code usage + if (blockType === 'function') { + logger.debug(`[resolveBlockReferences] Special handling for function input:`, + JSON.stringify(replacementValue, null, 2)); + formattedValue = JSON.stringify(replacementValue); + } + // For API blocks, handle body special case + else if (blockType === 'api') { + logger.debug(`[resolveBlockReferences] Special handling for API input:`, + JSON.stringify(replacementValue, null, 2)); + formattedValue = JSON.stringify(replacementValue); + } + // For condition blocks, ensure proper formatting + else if (blockType === 'condition') { + logger.debug(`[resolveBlockReferences] Special handling for condition input:`, + JSON.stringify(replacementValue, null, 2)); + formattedValue = this.stringifyForCondition(replacementValue); + } + // For all other blocks, stringify objects + else { + formattedValue = JSON.stringify(replacementValue); + } + } else { + // For primitive values + formattedValue = String(replacementValue); + } + } else { + // Standard handling for non-input references + formattedValue = typeof replacementValue === 'object' + ? JSON.stringify(replacementValue) + : String(replacementValue); + } + + logger.debug(`[resolveBlockReferences] Resolved value:`, formattedValue); + resolvedValue = resolvedValue.replace(match, formattedValue) + continue + } + } + } + // Special case for "loop" references - allows accessing loop properties if (blockRef.toLowerCase() === 'loop') { // Find which loop this block belongs to @@ -383,39 +502,6 @@ export class InputResolver { } } - // Special case for "start" references - // This allows users to reference the starter block using - // regardless of the actual name of the starter block - if (blockRef.toLowerCase() === 'start') { - // Find the starter block - const starterBlock = this.workflow.blocks.find((block) => block.metadata?.id === 'starter') - if (starterBlock) { - const blockState = context.blockStates.get(starterBlock.id) - if (blockState) { - // Navigate through the path parts - let replacementValue: any = blockState.output - for (const part of pathParts) { - if (!replacementValue || typeof replacementValue !== 'object') { - throw new Error(`Invalid path "${part}" in "${path}" for starter block.`) - } - replacementValue = replacementValue[part] - if (replacementValue === undefined) { - throw new Error(`No value found at path "${path}" in starter block.`) - } - } - - // Format the value - const formattedValue = - typeof replacementValue === 'object' - ? JSON.stringify(replacementValue) - : String(replacementValue) - - resolvedValue = resolvedValue.replace(match, formattedValue) - continue - } - } - } - // Standard block reference resolution let sourceBlock = this.blockById.get(blockRef) if (!sourceBlock) {