diff --git a/sim/app/api/schedules/execute/route.ts b/sim/app/api/schedules/execute/route.ts index 2db1be647c..8983475897 100644 --- a/sim/app/api/schedules/execute/route.ts +++ b/sim/app/api/schedules/execute/route.ts @@ -320,11 +320,33 @@ export async function GET(req: NextRequest) { ) logger.info(`[${requestId}] Executing workflow ${schedule.workflowId}`) + + // Get workflow variables + let workflowVariables = {} + if (workflowRecord.variables) { + try { + // Parse workflow variables if they're stored as a string + if (typeof workflowRecord.variables === 'string') { + workflowVariables = JSON.parse(workflowRecord.variables) + } else { + // Otherwise use as is (already parsed JSON) + workflowVariables = workflowRecord.variables + } + logger.debug(`[${requestId}] Loaded ${Object.keys(workflowVariables).length} workflow variables for: ${schedule.workflowId}`) + } catch (error) { + logger.error(`[${requestId}] Failed to parse workflow variables: ${schedule.workflowId}`, error) + // Continue execution even if variables can't be parsed + } + } else { + logger.debug(`[${requestId}] No workflow variables found for: ${schedule.workflowId}`) + } + const executor = new Executor( serializedWorkflow, processedBlockStates, // Use the processed block states decryptedEnvVars, - input + input, + workflowVariables ) const result = await executor.execute(schedule.workflowId) diff --git a/sim/app/api/webhooks/trigger/[path]/route.ts b/sim/app/api/webhooks/trigger/[path]/route.ts index 4c05e09bb1..b97e01c4d2 100644 --- a/sim/app/api/webhooks/trigger/[path]/route.ts +++ b/sim/app/api/webhooks/trigger/[path]/route.ts @@ -620,11 +620,32 @@ async function processWebhook( `[${requestId}] Starting workflow execution with ${Object.keys(processedBlockStates).length} blocks` ) + // Get workflow variables + let workflowVariables = {} + if (foundWorkflow.variables) { + try { + // Parse workflow variables if they're stored as a string + if (typeof foundWorkflow.variables === 'string') { + workflowVariables = JSON.parse(foundWorkflow.variables) + } else { + // Otherwise use as is (already parsed JSON) + workflowVariables = foundWorkflow.variables + } + logger.debug(`[${requestId}] Loaded ${Object.keys(workflowVariables).length} workflow variables for: ${foundWorkflow.id}`) + } catch (error) { + logger.error(`[${requestId}] Failed to parse workflow variables: ${foundWorkflow.id}`, error) + // Continue execution even if variables can't be parsed + } + } else { + logger.debug(`[${requestId}] No workflow variables found for: ${foundWorkflow.id}`) + } + const executor = new Executor( serializedWorkflow, processedBlockStates, decryptedEnvVars, - enrichedInput + enrichedInput, + workflowVariables ) const result = await executor.execute(foundWorkflow.id) diff --git a/sim/app/api/workflows/[id]/execute/route.test.ts b/sim/app/api/workflows/[id]/execute/route.test.ts index b767af1180..bbcb1e9592 100644 --- a/sim/app/api/workflows/[id]/execute/route.test.ts +++ b/sim/app/api/workflows/[id]/execute/route.test.ts @@ -265,7 +265,8 @@ describe('Workflow Execution API Route', () => { expect.anything(), expect.anything(), expect.anything(), - requestBody + requestBody, + expect.anything() // Expect workflow variables (5th parameter) ) }) @@ -308,7 +309,8 @@ describe('Workflow Execution API Route', () => { expect.anything(), expect.anything(), expect.anything(), - structuredInput + structuredInput, + expect.anything() // Expect workflow variables (5th parameter) ) }) @@ -342,7 +344,8 @@ describe('Workflow Execution API Route', () => { expect.anything(), expect.anything(), expect.anything(), - {} + {}, + expect.anything() // Expect workflow variables (5th parameter) ) }) @@ -447,4 +450,99 @@ describe('Workflow Execution API Route', () => { .persistExecutionError expect(persistExecutionError).toHaveBeenCalled() }) + + /** + * Test that workflow variables are properly passed to the Executor + */ + it('should pass workflow variables to the Executor', async () => { + // Create mock variables for the workflow + const workflowVariables = { + 'variable1': { id: 'var1', name: 'variable1', type: 'string', value: '"test value"' }, + 'variable2': { id: 'var2', name: 'variable2', type: 'boolean', value: 'true' } + } + + // Mock workflow with variables + vi.doMock('@/app/api/workflows/middleware', () => ({ + validateWorkflowAccess: vi.fn().mockResolvedValue({ + workflow: { + id: 'workflow-with-vars-id', + userId: 'user-id', + state: { + blocks: { + 'starter-id': { + id: 'starter-id', + type: 'starter', + name: 'Start', + position: { x: 100, y: 100 }, + enabled: true, + }, + 'agent-id': { + id: 'agent-id', + type: 'agent', + name: 'Agent', + position: { x: 300, y: 100 }, + enabled: true, + }, + }, + edges: [ + { + id: 'edge-1', + source: 'starter-id', + target: 'agent-id', + sourceHandle: 'source', + targetHandle: 'target', + }, + ], + loops: {}, + }, + variables: workflowVariables, + }, + }), + })) + + // Create a constructor mock to capture the arguments + const executorConstructorMock = vi.fn().mockImplementation(() => ({ + execute: vi.fn().mockResolvedValue({ + success: true, + output: { response: 'Execution completed with variables' }, + logs: [], + metadata: { + duration: 100, + startTime: new Date().toISOString(), + endTime: new Date().toISOString(), + }, + }), + })) + + // Override the executor mock + vi.doMock('@/executor', () => ({ + Executor: executorConstructorMock, + })) + + // Create a mock request + const req = createMockRequest('POST', { testInput: 'value' }) + + // Create params similar to what Next.js would provide + const params = Promise.resolve({ id: 'workflow-with-vars-id' }) + + // Import the handler after mocks are set up + const { POST } = await import('./route') + + // Call the handler + await POST(req, { params }) + + // Verify the Executor was constructed with workflow variables + expect(executorConstructorMock).toHaveBeenCalled() + + // Check that the 5th parameter (workflow variables) was passed + const executorCalls = executorConstructorMock.mock.calls + expect(executorCalls.length).toBeGreaterThan(0) + + // Each call to the constructor should have at least 5 parameters + const lastCall = executorCalls[executorCalls.length - 1] + expect(lastCall.length).toBeGreaterThanOrEqual(5) + + // The 5th parameter should be the workflow variables + expect(lastCall[4]).toEqual(workflowVariables) + }) }) diff --git a/sim/app/api/workflows/[id]/execute/route.ts b/sim/app/api/workflows/[id]/execute/route.ts index d30d4fc931..88e8d8008e 100644 --- a/sim/app/api/workflows/[id]/execute/route.ts +++ b/sim/app/api/workflows/[id]/execute/route.ts @@ -148,11 +148,38 @@ async function executeWorkflow(workflow: any, requestId: string, input?: any) { {} as Record> ) + // Get workflow variables + let workflowVariables = {} + if (workflow.variables) { + try { + // Parse workflow variables if they're stored as a string + if (typeof workflow.variables === 'string') { + workflowVariables = JSON.parse(workflow.variables) + } else { + // Otherwise use as is (already parsed JSON) + workflowVariables = workflow.variables + } + logger.debug(`[${requestId}] Loaded ${Object.keys(workflowVariables).length} workflow variables for: ${workflowId}`) + } catch (error) { + logger.error(`[${requestId}] Failed to parse workflow variables: ${workflowId}`, error) + // Continue execution even if variables can't be parsed + } + } else { + logger.debug(`[${requestId}] No workflow variables found for: ${workflowId}`) + } + // Serialize and execute the workflow logger.debug(`[${requestId}] Serializing workflow: ${workflowId}`) const serializedWorkflow = new Serializer().serializeWorkflow(mergedStates, edges, loops) - const executor = new Executor(serializedWorkflow, processedBlockStates, decryptedEnvVars, input) + const executor = new Executor( + serializedWorkflow, + processedBlockStates, + decryptedEnvVars, + input, + workflowVariables + ) + const result = await executor.execute(workflowId) logger.info(`[${requestId}] Workflow execution completed: ${workflowId}`, { diff --git a/sim/app/w/[id]/hooks/use-workflow-execution.ts b/sim/app/w/[id]/hooks/use-workflow-execution.ts index 02651ddb03..0b31f0510d 100644 --- a/sim/app/w/[id]/hooks/use-workflow-execution.ts +++ b/sim/app/w/[id]/hooks/use-workflow-execution.ts @@ -140,6 +140,7 @@ export function useWorkflowExecution() { workflow, currentBlockStates, envVarValues, + undefined, workflowVariables ) setExecutor(newExecutor) diff --git a/sim/executor/resolver.ts b/sim/executor/resolver.ts index 251d920830..62e6c0850d 100644 --- a/sim/executor/resolver.ts +++ b/sim/executor/resolver.ts @@ -51,6 +51,7 @@ 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)) { @@ -63,7 +64,7 @@ export class InputResolver { // Handle string values that may contain references if (typeof value === 'string') { // First check for variable references - let resolvedValue = this.resolveVariableReferences(value) + let resolvedValue = this.resolveVariableReferences(value, block) // Then resolve block references resolvedValue = this.resolveBlockReferences(resolvedValue, context, block) @@ -77,16 +78,23 @@ export class InputResolver { // Resolve environment variables resolvedValue = this.resolveEnvVariables(resolvedValue, isApiKey) - // Convert JSON strings to objects if possible - try { - if (resolvedValue.startsWith('{') || resolvedValue.startsWith('[')) { - result[key] = JSON.parse(resolvedValue) - } else { + // 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 + } + // For other inputs, try to convert JSON strings to objects + else { + try { + if (resolvedValue.startsWith('{') || resolvedValue.startsWith('[')) { + result[key] = JSON.parse(resolvedValue) + } else { + result[key] = resolvedValue + } + } catch { + // If it's not valid JSON, keep it as a string result[key] = resolvedValue } - } catch { - // If it's not valid JSON, keep it as a string - result[key] = resolvedValue } } // Handle objects and arrays recursively @@ -106,9 +114,10 @@ export class InputResolver { * Resolves workflow variable references in a string (). * * @param value - String containing variable references + * @param currentBlock - The current block, used to determine context * @returns String with resolved variable references */ - resolveVariableReferences(value: string): string { + resolveVariableReferences(value: string, currentBlock?: SerializedBlock): string { const variableMatches = value.match(/]+)>/g) if (!variableMatches) return value @@ -125,11 +134,57 @@ export class InputResolver { if (foundVariable) { const [_, variable] = foundVariable - // Format the value appropriately - const formattedValue = - typeof variable.value === 'object' - ? JSON.stringify(variable.value) - : String(variable.value) + + // Process variable value based on its type + let processedValue = variable.value + + // Handle string values that could be stored with quotes + if (variable.type === 'string' && typeof processedValue === 'string') { + // If the string value starts and ends with quotes, remove them + const trimmed = processedValue.trim() + if ((trimmed.startsWith('"') && trimmed.endsWith('"')) || + (trimmed.startsWith("'") && trimmed.endsWith("'"))) { + // Remove the quotes and unescape any escaped quotes + processedValue = trimmed.slice(1, -1).replace(/\\"/g, '"').replace(/\\'/g, "'") + } + } + // Handle boolean values that might be stored as strings + else if (variable.type === 'boolean' && typeof processedValue === 'string') { + processedValue = processedValue.trim().toLowerCase() === 'true' + } + // Handle number values that might be stored as strings + else if (variable.type === 'number' && typeof processedValue === 'string') { + const parsed = Number(processedValue) + if (!isNaN(parsed)) { + processedValue = parsed + } + } + // Handle object/array values that might be stored as JSON strings + else if ((variable.type === 'object' || variable.type === 'array') && + typeof processedValue === 'string') { + try { + processedValue = JSON.parse(processedValue) + } catch (e) { + // Keep as string if parsing fails + } + } + + // Determine if this needs to be a code-compatible string literal + const needsCodeStringLiteral = this.needsCodeStringLiteral(currentBlock, value); + + // Format the processed value for insertion into the string based on context + let formattedValue: string; + + if (variable.type === 'string' && needsCodeStringLiteral) { + // For code contexts like function and condition blocks, properly quote strings + formattedValue = JSON.stringify(processedValue); + } else if (typeof processedValue === 'object' && processedValue !== null) { + // For objects, always stringify + formattedValue = JSON.stringify(processedValue); + } else { + // For other types in normal contexts, use simple string conversion + formattedValue = String(processedValue); + } resolvedValue = resolvedValue.replace(match, formattedValue) } @@ -138,6 +193,56 @@ export class InputResolver { return resolvedValue } + /** + * Determines if a value needs to be formatted as a code-compatible string literal + * based on the block type and context. Handles JavaScript and other code contexts. + * + * @param block - The block where the value is being used + * @param expression - The expression containing the value + * @returns Whether the value should be formatted as a string literal + */ + private needsCodeStringLiteral(block?: SerializedBlock, expression?: string): boolean { + if (!block) return false; + + // These block types execute code and need properly formatted string literals + const codeExecutionBlocks = ['function', 'condition']; + + // Check if this is a block that executes code + if (block.metadata?.id && codeExecutionBlocks.includes(block.metadata.id)) { + return true; + } + + // Check if the expression is likely part of code + if (expression) { + const codeIndicators = [ + // Function/method calls + /\(\s*$/, // Function call + /\.\w+\s*\(/, // Method call + + // JavaScript/Python operators + /[=<>!+\-*/%](?:==?)?/, // Common operators + /\+=|-=|\*=|\/=|%=|\*\*=?/, // Assignment operators + + // JavaScript keywords + /\b(if|else|for|while|return|var|let|const|function)\b/, + + // Python keywords + /\b(if|else|elif|for|while|def|return|import|from|as|class|with|try|except)\b/, + + // Common code patterns + /^['"]use strict['"];?$/, // JS strict mode + /\$\{.+?\}/, // JS template literals + /f['"].*?['"]/, // Python f-strings + /\bprint\s*\(/, // Python print + /\bconsole\.\w+\(/ // JS console methods + ]; + + return codeIndicators.some(pattern => pattern.test(expression)); + } + + return false; + } + /** * Resolves block references in a string ( or ). * Handles inactive paths, missing blocks, and formats values appropriately. @@ -387,8 +492,8 @@ export class InputResolver { if (currentBlock.metadata?.id === 'condition') { formattedValue = this.stringifyForCondition(replacementValue) - } else if (currentBlock.metadata?.id === 'function' && typeof replacementValue === 'string') { - // For function blocks, we need to properly quote string values to avoid syntax errors + } else if (typeof replacementValue === 'string' && this.needsCodeStringLiteral(currentBlock, value)) { + // For code blocks, quote string values properly for the given language formattedValue = JSON.stringify(replacementValue) } else { formattedValue = @@ -518,7 +623,7 @@ export class InputResolver { // Handle strings if (typeof value === 'string') { // First resolve variable references - const resolvedVars = this.resolveVariableReferences(value) + const resolvedVars = this.resolveVariableReferences(value, currentBlock) // Then resolve block references const resolvedReferences = this.resolveBlockReferences(resolvedVars, context, currentBlock)