From 730164abee207a9d770c1c9cf9de48698cb02a16 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sat, 23 Aug 2025 13:15:12 -0700 Subject: [PATCH] fix(custom-tool): fix textarea, param dropdown for available params, validation for invalid schemas, variable resolution in custom tools and subflow tags (#1117) * fix(custom-tools): fix text area for custom tools * added param dropdown in agent custom tool * add syntax highlighting for params, fix dropdown styling * ux * add tooltip to prevent indicate invalid json schema on schema and code tabs * feat(custom-tool): added stricter JSON schema validation and error when saving json schema for custom tools * fix(custom-tool): allow variable resolution in custom tools * fix variable resolution in subflow tags * refactored function execution to use helpers * cleanup * fix block variable resolution to inject at runtime * fix highlighting code --------- Co-authored-by: Vikhyath Mondreti --- apps/sim/app/api/function/execute/route.ts | 139 +++++++- apps/sim/app/api/providers/route.ts | 7 + .../components/code-editor/code-editor.tsx | 94 +++++- .../custom-tool-modal/custom-tool-modal.tsx | 318 +++++++++++++++++- .../components/tool-input/tool-input.tsx | 3 +- .../executor/handlers/agent/agent-handler.ts | 45 +++ .../function/function-handler.test.ts | 3 + .../handlers/function/function-handler.ts | 43 ++- .../executor/handlers/loop/loop-handler.ts | 5 +- .../handlers/parallel/parallel-handler.ts | 5 +- apps/sim/executor/index.ts | 34 +- apps/sim/executor/types.ts | 1 + apps/sim/providers/anthropic/index.ts | 50 +-- apps/sim/providers/azure-openai/index.ts | 26 +- apps/sim/providers/cerebras/index.ts | 21 +- apps/sim/providers/deepseek/index.ts | 26 +- apps/sim/providers/groq/index.ts | 21 +- apps/sim/providers/types.ts | 3 + apps/sim/providers/utils.ts | 20 +- apps/sim/providers/xai/index.ts | 26 +- apps/sim/tools/function/execute.test.ts | 3 + apps/sim/tools/function/execute.ts | 8 + apps/sim/tools/function/types.ts | 1 + apps/sim/tools/index.ts | 14 +- apps/sim/tools/utils.test.ts | 6 + apps/sim/tools/utils.ts | 10 + 26 files changed, 688 insertions(+), 244 deletions(-) diff --git a/apps/sim/app/api/function/execute/route.ts b/apps/sim/app/api/function/execute/route.ts index 96d1d6ed3..08dfae068 100644 --- a/apps/sim/app/api/function/execute/route.ts +++ b/apps/sim/app/api/function/execute/route.ts @@ -213,24 +213,81 @@ function createUserFriendlyErrorMessage( } /** - * Resolves environment variables and tags in code - * @param code - Code with variables - * @param params - Parameters that may contain variable values - * @param envVars - Environment variables from the workflow - * @returns Resolved code + * Resolves workflow variables with syntax */ +function resolveWorkflowVariables( + code: string, + workflowVariables: Record, + contextVariables: Record +): string { + let resolvedCode = code -function resolveCodeVariables( + const variableMatches = resolvedCode.match(/]+)>/g) || [] + for (const match of variableMatches) { + const variableName = match.slice(' (variable.name || '').replace(/\s+/g, '') === variableName + ) + + if (foundVariable) { + const variable = foundVariable[1] + // Get the typed value - handle different variable types + let variableValue = variable.value + + if (variable.value !== undefined && variable.value !== null) { + try { + // Handle 'string' type the same as 'plain' for backward compatibility + const type = variable.type === 'string' ? 'plain' : variable.type + + // For plain text, use exactly what's entered without modifications + if (type === 'plain' && typeof variableValue === 'string') { + // Use as-is for plain text + } else if (type === 'number') { + variableValue = Number(variableValue) + } else if (type === 'boolean') { + variableValue = variableValue === 'true' || variableValue === true + } else if (type === 'json') { + try { + variableValue = + typeof variableValue === 'string' ? JSON.parse(variableValue) : variableValue + } catch { + // Keep original value if JSON parsing fails + } + } + } catch (error) { + // Fallback to original value on error + variableValue = variable.value + } + } + + // Create a safe variable reference + const safeVarName = `__variable_${variableName.replace(/[^a-zA-Z0-9_]/g, '_')}` + contextVariables[safeVarName] = variableValue + + // Replace the variable reference with the safe variable name + resolvedCode = resolvedCode.replace(new RegExp(escapeRegExp(match), 'g'), safeVarName) + } else { + // Variable not found - replace with empty string to avoid syntax errors + resolvedCode = resolvedCode.replace(new RegExp(escapeRegExp(match), 'g'), '') + } + } + + return resolvedCode +} + +/** + * Resolves environment variables with {{var_name}} syntax + */ +function resolveEnvironmentVariables( code: string, params: Record, - envVars: Record = {}, - blockData: Record = {}, - blockNameMapping: Record = {} -): { resolvedCode: string; contextVariables: Record } { + envVars: Record, + contextVariables: Record +): string { let resolvedCode = code - const contextVariables: Record = {} - // Resolve environment variables with {{var_name}} syntax const envVarMatches = resolvedCode.match(/\{\{([^}]+)\}\}/g) || [] for (const match of envVarMatches) { const varName = match.slice(2, -2).trim() @@ -245,7 +302,21 @@ function resolveCodeVariables( resolvedCode = resolvedCode.replace(new RegExp(escapeRegExp(match), 'g'), safeVarName) } - // Resolve tags with syntax (including nested paths like ) + return resolvedCode +} + +/** + * Resolves tags with syntax (including nested paths like ) + */ +function resolveTagVariables( + code: string, + params: Record, + blockData: Record, + blockNameMapping: Record, + contextVariables: Record +): string { + let resolvedCode = code + const tagMatches = resolvedCode.match(/<([a-zA-Z_][a-zA-Z0-9_.]*[a-zA-Z0-9_])>/g) || [] for (const match of tagMatches) { @@ -300,6 +371,42 @@ function resolveCodeVariables( resolvedCode = resolvedCode.replace(new RegExp(escapeRegExp(match), 'g'), safeVarName) } + return resolvedCode +} + +/** + * Resolves environment variables and tags in code + * @param code - Code with variables + * @param params - Parameters that may contain variable values + * @param envVars - Environment variables from the workflow + * @returns Resolved code + */ +function resolveCodeVariables( + code: string, + params: Record, + envVars: Record = {}, + blockData: Record = {}, + blockNameMapping: Record = {}, + workflowVariables: Record = {} +): { resolvedCode: string; contextVariables: Record } { + let resolvedCode = code + const contextVariables: Record = {} + + // Resolve workflow variables with syntax first + resolvedCode = resolveWorkflowVariables(resolvedCode, workflowVariables, contextVariables) + + // Resolve environment variables with {{var_name}} syntax + resolvedCode = resolveEnvironmentVariables(resolvedCode, params, envVars, contextVariables) + + // Resolve tags with syntax (including nested paths like ) + resolvedCode = resolveTagVariables( + resolvedCode, + params, + blockData, + blockNameMapping, + contextVariables + ) + return { resolvedCode, contextVariables } } @@ -338,6 +445,7 @@ export async function POST(req: NextRequest) { envVars = {}, blockData = {}, blockNameMapping = {}, + workflowVariables = {}, workflowId, isCustomTool = false, } = body @@ -360,7 +468,8 @@ export async function POST(req: NextRequest) { executionParams, envVars, blockData, - blockNameMapping + blockNameMapping, + workflowVariables ) resolvedCode = codeResolution.resolvedCode const contextVariables = codeResolution.contextVariables @@ -368,8 +477,8 @@ export async function POST(req: NextRequest) { const executionMethod = 'vm' // Default execution method logger.info(`[${requestId}] Using VM for code execution`, { - resolvedCode, hasEnvVars: Object.keys(envVars).length > 0, + hasWorkflowVariables: Object.keys(workflowVariables).length > 0, }) // Create a secure context with console logging diff --git a/apps/sim/app/api/providers/route.ts b/apps/sim/app/api/providers/route.ts index 17d37a4f3..8aa62f7e7 100644 --- a/apps/sim/app/api/providers/route.ts +++ b/apps/sim/app/api/providers/route.ts @@ -39,6 +39,9 @@ export async function POST(request: NextRequest) { stream, messages, environmentVariables, + workflowVariables, + blockData, + blockNameMapping, reasoningEffort, verbosity, } = body @@ -60,6 +63,7 @@ export async function POST(request: NextRequest) { messageCount: messages?.length || 0, hasEnvironmentVariables: !!environmentVariables && Object.keys(environmentVariables).length > 0, + hasWorkflowVariables: !!workflowVariables && Object.keys(workflowVariables).length > 0, reasoningEffort, verbosity, }) @@ -103,6 +107,9 @@ export async function POST(request: NextRequest) { stream, messages, environmentVariables, + workflowVariables, + blockData, + blockNameMapping, reasoningEffort, verbosity, }) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/tool-input/components/code-editor/code-editor.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/tool-input/components/code-editor/code-editor.tsx index 9ab634e66..1ea5279d5 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/tool-input/components/code-editor/code-editor.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/tool-input/components/code-editor/code-editor.tsx @@ -18,6 +18,7 @@ interface CodeEditorProps { highlightVariables?: boolean onKeyDown?: (e: React.KeyboardEvent) => void disabled?: boolean + schemaParameters?: Array<{ name: string; type: string; description: string; required: boolean }> } export function CodeEditor({ @@ -30,6 +31,7 @@ export function CodeEditor({ highlightVariables = true, onKeyDown, disabled = false, + schemaParameters = [], }: CodeEditorProps) { const [code, setCode] = useState(value) const [visualLineHeights, setVisualLineHeights] = useState([]) @@ -120,25 +122,80 @@ export function CodeEditor({ // First, get the default Prism highlighting let highlighted = highlight(code, languages[language], language) - // Then, highlight environment variables with {{var_name}} syntax in blue - if (highlighted.includes('{{')) { - highlighted = highlighted.replace( - /\{\{([^}]+)\}\}/g, - '{{$1}}' - ) + // Collect all syntax highlights to apply in a single pass + type SyntaxHighlight = { + start: number + end: number + replacement: string } + const highlights: SyntaxHighlight[] = [] - // Also highlight tags with syntax in blue - if (highlighted.includes('<') && !language.includes('html')) { - highlighted = highlighted.replace(/<([^>\s/]+)>/g, (match, group) => { - // Avoid replacing HTML tags in comments - if (match.startsWith('