diff --git a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts index fb1598fc2..f0acdb281 100644 --- a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts +++ b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts @@ -817,6 +817,8 @@ function normalizeResponseFormat(value: any): string { interface EdgeHandleValidationResult { valid: boolean error?: string + /** The normalized handle to use (e.g., simple 'if' normalized to 'condition-{uuid}') */ + normalizedHandle?: string } /** @@ -851,13 +853,6 @@ function validateSourceHandleForBlock( } case 'condition': { - if (!sourceHandle.startsWith(EDGE.CONDITION_PREFIX)) { - return { - valid: false, - error: `Invalid source handle "${sourceHandle}" for condition block. Must start with "${EDGE.CONDITION_PREFIX}"`, - } - } - const conditionsValue = sourceBlock?.subBlocks?.conditions?.value if (!conditionsValue) { return { @@ -866,6 +861,8 @@ function validateSourceHandleForBlock( } } + // validateConditionHandle accepts simple format (if, else-if-0, else), + // legacy format (condition-{blockId}-if), and internal ID format (condition-{uuid}) return validateConditionHandle(sourceHandle, sourceBlock.id, conditionsValue) } @@ -879,13 +876,6 @@ function validateSourceHandleForBlock( } case 'router_v2': { - if (!sourceHandle.startsWith(EDGE.ROUTER_PREFIX)) { - return { - valid: false, - error: `Invalid source handle "${sourceHandle}" for router_v2 block. Must start with "${EDGE.ROUTER_PREFIX}"`, - } - } - const routesValue = sourceBlock?.subBlocks?.routes?.value if (!routesValue) { return { @@ -894,6 +884,8 @@ function validateSourceHandleForBlock( } } + // validateRouterHandle accepts simple format (route-0, route-1), + // legacy format (router-{blockId}-route-1), and internal ID format (router-{uuid}) return validateRouterHandle(sourceHandle, sourceBlock.id, routesValue) } @@ -910,7 +902,12 @@ function validateSourceHandleForBlock( /** * Validates condition handle references a valid condition in the block. - * Accepts both internal IDs (condition-blockId-if) and semantic keys (condition-blockId-else-if) + * Accepts multiple formats: + * - Simple format: "if", "else-if-0", "else-if-1", "else" + * - Legacy semantic format: "condition-{blockId}-if", "condition-{blockId}-else-if" + * - Internal ID format: "condition-{conditionId}" + * + * Returns the normalized handle (condition-{conditionId}) for storage. */ function validateConditionHandle( sourceHandle: string, @@ -943,48 +940,77 @@ function validateConditionHandle( } } - const validHandles = new Set() - const semanticPrefix = `condition-${blockId}-` - let elseIfCount = 0 + // Build a map of all valid handle formats -> normalized handle (condition-{conditionId}) + const handleToNormalized = new Map() + const legacySemanticPrefix = `condition-${blockId}-` + let elseIfIndex = 0 for (const condition of conditions) { - if (condition.id) { - validHandles.add(`condition-${condition.id}`) - } + if (!condition.id) continue + const normalizedHandle = `condition-${condition.id}` + const title = condition.title?.toLowerCase() + + // Always accept internal ID format + handleToNormalized.set(normalizedHandle, normalizedHandle) + + if (title === 'if') { + // Simple format: "if" + handleToNormalized.set('if', normalizedHandle) + // Legacy format: "condition-{blockId}-if" + handleToNormalized.set(`${legacySemanticPrefix}if`, normalizedHandle) + } else if (title === 'else if') { + // Simple format: "else-if-0", "else-if-1", etc. (0-indexed) + handleToNormalized.set(`else-if-${elseIfIndex}`, normalizedHandle) + // Legacy format: "condition-{blockId}-else-if" for first, "condition-{blockId}-else-if-2" for second + if (elseIfIndex === 0) { + handleToNormalized.set(`${legacySemanticPrefix}else-if`, normalizedHandle) + } else { + handleToNormalized.set(`${legacySemanticPrefix}else-if-${elseIfIndex + 1}`, normalizedHandle) + } + elseIfIndex++ + } else if (title === 'else') { + // Simple format: "else" + handleToNormalized.set('else', normalizedHandle) + // Legacy format: "condition-{blockId}-else" + handleToNormalized.set(`${legacySemanticPrefix}else`, normalizedHandle) + } + } + + const normalizedHandle = handleToNormalized.get(sourceHandle) + if (normalizedHandle) { + return { valid: true, normalizedHandle } + } + + // Build list of valid simple format options for error message + const simpleOptions: string[] = [] + elseIfIndex = 0 + for (const condition of conditions) { const title = condition.title?.toLowerCase() if (title === 'if') { - validHandles.add(`${semanticPrefix}if`) + simpleOptions.push('if') } else if (title === 'else if') { - elseIfCount++ - validHandles.add( - elseIfCount === 1 ? `${semanticPrefix}else-if` : `${semanticPrefix}else-if-${elseIfCount}` - ) + simpleOptions.push(`else-if-${elseIfIndex}`) + elseIfIndex++ } else if (title === 'else') { - validHandles.add(`${semanticPrefix}else`) + simpleOptions.push('else') } } - if (validHandles.has(sourceHandle)) { - return { valid: true } - } - - const validOptions = Array.from(validHandles).slice(0, 5) - const moreCount = validHandles.size - validOptions.length - let validOptionsStr = validOptions.join(', ') - if (moreCount > 0) { - validOptionsStr += `, ... and ${moreCount} more` - } - return { valid: false, - error: `Invalid condition handle "${sourceHandle}". Valid handles: ${validOptionsStr}`, + error: `Invalid condition handle "${sourceHandle}". Valid handles: ${simpleOptions.join(', ')}`, } } /** * Validates router handle references a valid route in the block. - * Accepts both internal IDs (router-{routeId}) and semantic keys (router-{blockId}-route-1) + * Accepts multiple formats: + * - Simple format: "route-0", "route-1", "route-2" (0-indexed) + * - Legacy semantic format: "router-{blockId}-route-1" (1-indexed) + * - Internal ID format: "router-{routeId}" + * + * Returns the normalized handle (router-{routeId}) for storage. */ function validateRouterHandle( sourceHandle: string, @@ -1017,47 +1043,48 @@ function validateRouterHandle( } } - const validHandles = new Set() - const semanticPrefix = `router-${blockId}-` + // Build a map of all valid handle formats -> normalized handle (router-{routeId}) + const handleToNormalized = new Map() + const legacySemanticPrefix = `router-${blockId}-` for (let i = 0; i < routes.length; i++) { const route = routes[i] + if (!route.id) continue - // Accept internal ID format: router-{uuid} - if (route.id) { - validHandles.add(`router-${route.id}`) - } + const normalizedHandle = `router-${route.id}` - // Accept 1-indexed route number format: router-{blockId}-route-1, router-{blockId}-route-2, etc. - validHandles.add(`${semanticPrefix}route-${i + 1}`) + // Always accept internal ID format: router-{uuid} + handleToNormalized.set(normalizedHandle, normalizedHandle) + + // Simple format: route-0, route-1, etc. (0-indexed) + handleToNormalized.set(`route-${i}`, normalizedHandle) + + // Legacy 1-indexed route number format: router-{blockId}-route-1 + handleToNormalized.set(`${legacySemanticPrefix}route-${i + 1}`, normalizedHandle) // Accept normalized title format: router-{blockId}-{normalized-title} - // Normalize: lowercase, replace spaces with dashes, remove special chars if (route.title && typeof route.title === 'string') { const normalizedTitle = route.title .toLowerCase() .replace(/\s+/g, '-') .replace(/[^a-z0-9-]/g, '') if (normalizedTitle) { - validHandles.add(`${semanticPrefix}${normalizedTitle}`) + handleToNormalized.set(`${legacySemanticPrefix}${normalizedTitle}`, normalizedHandle) } } } - if (validHandles.has(sourceHandle)) { - return { valid: true } + const normalizedHandle = handleToNormalized.get(sourceHandle) + if (normalizedHandle) { + return { valid: true, normalizedHandle } } - const validOptions = Array.from(validHandles).slice(0, 5) - const moreCount = validHandles.size - validOptions.length - let validOptionsStr = validOptions.join(', ') - if (moreCount > 0) { - validOptionsStr += `, ... and ${moreCount} more` - } + // Build list of valid simple format options for error message + const simpleOptions = routes.map((_, i) => `route-${i}`) return { valid: false, - error: `Invalid router handle "${sourceHandle}". Valid handles: ${validOptionsStr}`, + error: `Invalid router handle "${sourceHandle}". Valid handles: ${simpleOptions.join(', ')}`, } } @@ -1172,10 +1199,13 @@ function createValidatedEdge( return false } + // Use normalized handle if available (e.g., 'if' -> 'condition-{uuid}') + const finalSourceHandle = sourceValidation.normalizedHandle || sourceHandle + modifiedState.edges.push({ id: crypto.randomUUID(), source: sourceBlockId, - sourceHandle, + sourceHandle: finalSourceHandle, target: targetBlockId, targetHandle, type: 'default', @@ -1184,7 +1214,11 @@ function createValidatedEdge( } /** - * Adds connections as edges for a block + * Adds connections as edges for a block. + * Supports multiple target formats: + * - String: "target-block-id" + * - Object: { block: "target-block-id", handle?: "custom-target-handle" } + * - Array of strings or objects */ function addConnectionsAsEdges( modifiedState: any, @@ -1194,19 +1228,34 @@ function addConnectionsAsEdges( skippedItems?: SkippedItem[] ): void { Object.entries(connections).forEach(([sourceHandle, targets]) => { - const targetArray = Array.isArray(targets) ? targets : [targets] - targetArray.forEach((targetId: string) => { + if (targets === null) return + + const addEdgeForTarget = (targetBlock: string, targetHandle?: string) => { createValidatedEdge( modifiedState, blockId, - targetId, + targetBlock, sourceHandle, - 'target', + targetHandle || 'target', 'add_edge', logger, skippedItems ) - }) + } + + if (typeof targets === 'string') { + addEdgeForTarget(targets) + } else if (Array.isArray(targets)) { + targets.forEach((target: any) => { + if (typeof target === 'string') { + addEdgeForTarget(target) + } else if (target?.block) { + addEdgeForTarget(target.block, target.handle) + } + }) + } else if (typeof targets === 'object' && targets?.block) { + addEdgeForTarget(targets.block, targets.handle) + } }) } diff --git a/apps/sim/lib/workflows/sanitization/json-sanitizer.ts b/apps/sim/lib/workflows/sanitization/json-sanitizer.ts index d67b2cd34..95deaf2c0 100644 --- a/apps/sim/lib/workflows/sanitization/json-sanitizer.ts +++ b/apps/sim/lib/workflows/sanitization/json-sanitizer.ts @@ -269,11 +269,12 @@ function sanitizeSubBlocks( } /** - * Convert internal condition handle (condition-{uuid}) to semantic format (condition-{blockId}-if) + * Convert internal condition handle (condition-{uuid}) to simple format (if, else-if-0, else) + * Uses 0-indexed numbering for else-if conditions */ -function convertConditionHandleToSemantic( +function convertConditionHandleToSimple( handle: string, - blockId: string, + _blockId: string, block: BlockState ): string { if (!handle.startsWith('condition-')) { @@ -300,27 +301,24 @@ function convertConditionHandleToSemantic( return handle } - // Find the condition by ID and generate semantic handle - let elseIfCount = 0 + // Find the condition by ID and generate simple handle + let elseIfIndex = 0 for (const condition of conditions) { const title = condition.title?.toLowerCase() if (condition.id === conditionId) { if (title === 'if') { - return `condition-${blockId}-if` + return 'if' } if (title === 'else if') { - elseIfCount++ - return elseIfCount === 1 - ? `condition-${blockId}-else-if` - : `condition-${blockId}-else-if-${elseIfCount}` + return `else-if-${elseIfIndex}` } if (title === 'else') { - return `condition-${blockId}-else` + return 'else' } } - // Count else-ifs as we iterate + // Count else-ifs as we iterate (for index tracking) if (title === 'else if') { - elseIfCount++ + elseIfIndex++ } } @@ -329,9 +327,14 @@ function convertConditionHandleToSemantic( } /** - * Convert internal router handle (router-{uuid}) to semantic format (router-{blockId}-route-N) + * Convert internal router handle (router-{uuid}) to simple format (route-0, route-1) + * Uses 0-indexed numbering for routes */ -function convertRouterHandleToSemantic(handle: string, blockId: string, block: BlockState): string { +function convertRouterHandleToSimple( + handle: string, + _blockId: string, + block: BlockState +): string { if (!handle.startsWith('router-')) { return handle } @@ -356,10 +359,10 @@ function convertRouterHandleToSemantic(handle: string, blockId: string, block: B return handle } - // Find the route by ID and generate semantic handle (1-indexed) + // Find the route by ID and generate simple handle (0-indexed) for (let i = 0; i < routes.length; i++) { if (routes[i].id === routeId) { - return `router-${blockId}-route-${i + 1}` + return `route-${i}` } } @@ -368,15 +371,16 @@ function convertRouterHandleToSemantic(handle: string, blockId: string, block: B } /** - * Convert source handle to semantic format for condition and router blocks + * Convert source handle to simple format for condition and router blocks + * Outputs: if, else-if-0, else (for conditions) and route-0, route-1 (for routers) */ -function convertToSemanticHandle(handle: string, blockId: string, block: BlockState): string { +function convertToSimpleHandle(handle: string, blockId: string, block: BlockState): string { if (handle.startsWith('condition-') && block.type === 'condition') { - return convertConditionHandleToSemantic(handle, blockId, block) + return convertConditionHandleToSimple(handle, blockId, block) } if (handle.startsWith('router-') && block.type === 'router_v2') { - return convertRouterHandleToSemantic(handle, blockId, block) + return convertRouterHandleToSimple(handle, blockId, block) } return handle @@ -400,12 +404,12 @@ function extractConnectionsForBlock( return undefined } - // Group by source handle (converting to semantic format) + // Group by source handle (converting to simple format) for (const edge of outgoingEdges) { let handle = edge.sourceHandle || 'source' - // Convert internal UUID handles to semantic format - handle = convertToSemanticHandle(handle, blockId, block) + // Convert internal UUID handles to simple format (if, else-if-0, route-0, etc.) + handle = convertToSimpleHandle(handle, blockId, block) if (!connections[handle]) { connections[handle] = []