From 4defcc0418962c7aa1fd062accb56549be260534 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Thu, 18 Dec 2025 10:41:41 -0800 Subject: [PATCH 1/4] Add edge handle validation --- .../tools/server/workflow/edit-workflow.ts | 320 +++++++++++++++++- 1 file changed, 318 insertions(+), 2 deletions(-) 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 826f5531ec..5fda57485d 100644 --- a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts +++ b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts @@ -50,6 +50,8 @@ type SkippedItemType = | 'invalid_block_type' | 'invalid_edge_target' | 'invalid_edge_source' + | 'invalid_source_handle' + | 'invalid_target_handle' | 'invalid_subblock_field' | 'missing_required_params' | 'invalid_subflow_parent' @@ -734,6 +736,256 @@ function normalizeResponseFormat(value: any): string { } } +/** + * Result of edge handle validation + */ +interface EdgeHandleValidationResult { + valid: boolean + error?: string +} + +/** + * Validates that a source handle is valid for the given block type + * This is not hardcoded - it validates the format/pattern expected for each block type + */ +function validateSourceHandleForBlock( + sourceHandle: string, + sourceBlockType: string, + sourceBlock: any +): EdgeHandleValidationResult { + // All blocks can have an 'error' handle for error paths + if (sourceHandle === 'error') { + return { valid: true } + } + + switch (sourceBlockType) { + case 'loop': + // Loop blocks can only have loop-start-source and loop-end-source handles + if (sourceHandle === 'loop-start-source' || sourceHandle === 'loop-end-source') { + return { valid: true } + } + return { + valid: false, + error: `Invalid source handle "${sourceHandle}" for loop block. Valid handles are: loop-start-source, loop-end-source, error`, + } + + case 'parallel': + // Parallel blocks can only have parallel-start-source and parallel-end-source handles + if (sourceHandle === 'parallel-start-source' || sourceHandle === 'parallel-end-source') { + return { valid: true } + } + return { + valid: false, + error: `Invalid source handle "${sourceHandle}" for parallel block. Valid handles are: parallel-start-source, parallel-end-source, error`, + } + + case 'condition': { + // Condition blocks must have handles that start with 'condition-' + // The format is: condition-{conditionId} where conditionId is the full ID from the conditions array + // Example: condition-blockId-if, condition-blockId-else-if-1234567890, condition-blockId-else + if (!sourceHandle.startsWith('condition-')) { + return { + valid: false, + error: `Invalid source handle "${sourceHandle}" for condition block. Condition block handles must start with "condition-"`, + } + } + + // Validate the handle references a valid condition in the block + const conditionsValue = sourceBlock?.subBlocks?.conditions?.value + if (!conditionsValue) { + return { + valid: false, + error: `Invalid condition handle "${sourceHandle}" - condition block has no conditions defined`, + } + } + + return validateConditionHandleAgainstBlock(sourceHandle, sourceBlock.id, conditionsValue) + } + + case 'router': + // Router blocks can have router- prefixed handles or source + if (sourceHandle === 'source' || sourceHandle.startsWith('router-')) { + return { valid: true } + } + return { + valid: false, + error: `Invalid source handle "${sourceHandle}" for router block. Valid handles are: source, router-{targetId}, error`, + } + + default: + // Regular blocks can only have 'source' handle for success paths + if (sourceHandle === 'source') { + return { valid: true } + } + return { + valid: false, + error: `Invalid source handle "${sourceHandle}" for ${sourceBlockType} block. Valid handles are: source, error`, + } + } +} + +/** + * Validates that a condition handle references a valid condition in the block + * Handles both semantic keys (if, else-if, else) and internal timestamp-based IDs + * + * Condition handles have the format: condition-{conditionId} + * where conditionId is the full ID from the conditions array (e.g., "blockId-if", "blockId-else-if-timestamp") + * + * For copilot, semantic keys are translated: + * - "if" -> blockId-if + * - "else-if" or "else-if-N" -> matches by position in the conditions array + * - "else" -> blockId-else + */ +function validateConditionHandleAgainstBlock( + sourceHandle: string, + blockId: string, + conditionsValue: string | any[] +): EdgeHandleValidationResult { + // Parse conditions if it's a JSON string + let conditions: any[] + if (typeof conditionsValue === 'string') { + try { + conditions = JSON.parse(conditionsValue) + } catch { + // Can't parse conditions - this is an error state + return { + valid: false, + error: `Cannot validate condition handle "${sourceHandle}" - conditions value is not valid JSON`, + } + } + } else if (Array.isArray(conditionsValue)) { + conditions = conditionsValue + } else { + return { + valid: false, + error: `Cannot validate condition handle "${sourceHandle}" - conditions value is not an array`, + } + } + + if (!Array.isArray(conditions) || conditions.length === 0) { + return { + valid: false, + error: `Invalid condition handle "${sourceHandle}" - condition block has no conditions defined`, + } + } + + // Build set of all valid handles for this condition block + const validHandles = new Set() + + // Add handles based on actual condition IDs from the block + // Condition IDs are in format: blockId-if, blockId-else-if-timestamp, blockId-else + for (const condition of conditions) { + if (condition.id) { + // The handle format is: condition-{conditionId} + validHandles.add(`condition-${condition.id}`) + } + } + + // Also add semantic key handles that copilot might use + // These get translated to the actual IDs by createConditionHandle in parsing-utils.ts + // Format: condition-{blockId}-{semanticKey} + const semanticPrefix = `condition-${blockId}-` + + // Map conditions to their semantic keys based on title + let elseIfCount = 0 + for (const condition of conditions) { + const title = condition.title?.toLowerCase() + if (title === 'if') { + validHandles.add(`${semanticPrefix}if`) + } else if (title === 'else if') { + elseIfCount++ + if (elseIfCount === 1) { + validHandles.add(`${semanticPrefix}else-if`) + } else { + validHandles.add(`${semanticPrefix}else-if-${elseIfCount}`) + } + } else if (title === 'else') { + validHandles.add(`${semanticPrefix}else`) + } + } + + // Check if the provided handle is valid + if (validHandles.has(sourceHandle)) { + return { valid: true } + } + + // Build a helpful error message with the valid options + const validOptions = Array.from(validHandles).slice(0, 5) // Show first 5 options + 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}" - does not match any condition in the block. Valid handles: ${validOptionsStr}`, + } +} + +/** + * Validates that a target handle is valid + * Currently all blocks accept 'target' as the target handle + */ +function validateTargetHandle(targetHandle: string): EdgeHandleValidationResult { + // Standard target handle + if (targetHandle === 'target') { + return { valid: true } + } + + // Allow undefined/null which defaults to 'target' + if (!targetHandle) { + return { valid: true } + } + + return { + valid: false, + error: `Invalid target handle "${targetHandle}". Expected "target"`, + } +} + +/** + * Validates an edge's source and target handles against the workflow state + * Returns validation result with any errors + */ +function validateEdgeHandles( + sourceBlockId: string, + targetBlockId: string, + sourceHandle: string, + targetHandle: string, + modifiedState: any +): EdgeHandleValidationResult { + const sourceBlock = modifiedState.blocks[sourceBlockId] + if (!sourceBlock) { + return { + valid: false, + error: `Source block "${sourceBlockId}" not found`, + } + } + + const sourceBlockType = sourceBlock.type + if (!sourceBlockType) { + return { + valid: false, + error: `Source block "${sourceBlockId}" has no type`, + } + } + + // Validate source handle + const sourceValidation = validateSourceHandleForBlock(sourceHandle, sourceBlockType, sourceBlock) + if (!sourceValidation.valid) { + return sourceValidation + } + + // Validate target handle + const targetValidation = validateTargetHandle(targetHandle) + if (!targetValidation.valid) { + return targetValidation + } + + return { valid: true } +} + /** * Helper to add connections as edges for a block */ @@ -767,12 +1019,44 @@ function addConnectionsAsEdges( }) return } + + // Validate source and target handles are valid for the block types + const targetHandle = 'target' + const handleValidation = validateEdgeHandles( + blockId, + targetId, + sourceHandle, + targetHandle, + modifiedState + ) + + if (!handleValidation.valid) { + logger.warn( + `Invalid edge handle when creating connection from "${blockId}" to "${targetId}". Edge skipped.`, + { + sourceBlockId: blockId, + targetBlockId: targetId, + sourceHandle, + targetHandle, + error: handleValidation.error, + } + ) + skippedItems?.push({ + type: 'invalid_source_handle', + operationType: 'add_edge', + blockId: blockId, + reason: handleValidation.error || `Invalid handle for edge from "${blockId}" to "${targetId}"`, + details: { sourceHandle, targetHandle, targetId }, + }) + return + } + modifiedState.edges.push({ id: crypto.randomUUID(), source: blockId, sourceHandle, target: targetId, - targetHandle: 'target', + targetHandle, type: 'default', }) }) @@ -1296,12 +1580,44 @@ function applyOperationsToWorkflowState( }) return } + + // Validate source and target handles are valid for the block types + const resolvedTargetHandle = targetHandle || 'target' + const handleValidation = validateEdgeHandles( + block_id, + targetBlock, + actualSourceHandle, + resolvedTargetHandle, + modifiedState + ) + + if (!handleValidation.valid) { + logger.warn( + `Invalid edge handle when creating connection from "${block_id}" to "${targetBlock}". Edge skipped.`, + { + sourceBlockId: block_id, + targetBlockId: targetBlock, + sourceHandle: actualSourceHandle, + targetHandle: resolvedTargetHandle, + error: handleValidation.error, + } + ) + logSkippedItem(skippedItems, { + type: 'invalid_source_handle', + operationType: 'edit', + blockId: block_id, + reason: handleValidation.error || `Invalid handle for edge from "${block_id}" to "${targetBlock}"`, + details: { sourceHandle: actualSourceHandle, targetHandle: resolvedTargetHandle, targetId: targetBlock }, + }) + return + } + modifiedState.edges.push({ id: crypto.randomUUID(), source: block_id, sourceHandle: actualSourceHandle, target: targetBlock, - targetHandle: targetHandle || 'target', + targetHandle: resolvedTargetHandle, type: 'default', }) } From f84b7f9b6eef94b885ab41fb34539b58ca43d928 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Thu, 18 Dec 2025 10:46:00 -0800 Subject: [PATCH 2/4] Clean --- .../tools/server/workflow/edit-workflow.ts | 338 +++++++----------- 1 file changed, 126 insertions(+), 212 deletions(-) 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 5fda57485d..75daa61e53 100644 --- a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts +++ b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts @@ -736,121 +736,98 @@ function normalizeResponseFormat(value: any): string { } } -/** - * Result of edge handle validation - */ interface EdgeHandleValidationResult { valid: boolean error?: string } /** - * Validates that a source handle is valid for the given block type - * This is not hardcoded - it validates the format/pattern expected for each block type + * Validates source handle is valid for the block type */ function validateSourceHandleForBlock( sourceHandle: string, sourceBlockType: string, sourceBlock: any ): EdgeHandleValidationResult { - // All blocks can have an 'error' handle for error paths if (sourceHandle === 'error') { return { valid: true } } switch (sourceBlockType) { case 'loop': - // Loop blocks can only have loop-start-source and loop-end-source handles if (sourceHandle === 'loop-start-source' || sourceHandle === 'loop-end-source') { return { valid: true } } return { valid: false, - error: `Invalid source handle "${sourceHandle}" for loop block. Valid handles are: loop-start-source, loop-end-source, error`, + error: `Invalid source handle "${sourceHandle}" for loop block. Valid handles: loop-start-source, loop-end-source, error`, } case 'parallel': - // Parallel blocks can only have parallel-start-source and parallel-end-source handles if (sourceHandle === 'parallel-start-source' || sourceHandle === 'parallel-end-source') { return { valid: true } } return { valid: false, - error: `Invalid source handle "${sourceHandle}" for parallel block. Valid handles are: parallel-start-source, parallel-end-source, error`, + error: `Invalid source handle "${sourceHandle}" for parallel block. Valid handles: parallel-start-source, parallel-end-source, error`, } case 'condition': { - // Condition blocks must have handles that start with 'condition-' - // The format is: condition-{conditionId} where conditionId is the full ID from the conditions array - // Example: condition-blockId-if, condition-blockId-else-if-1234567890, condition-blockId-else if (!sourceHandle.startsWith('condition-')) { return { valid: false, - error: `Invalid source handle "${sourceHandle}" for condition block. Condition block handles must start with "condition-"`, + error: `Invalid source handle "${sourceHandle}" for condition block. Must start with "condition-"`, } } - // Validate the handle references a valid condition in the block const conditionsValue = sourceBlock?.subBlocks?.conditions?.value if (!conditionsValue) { return { valid: false, - error: `Invalid condition handle "${sourceHandle}" - condition block has no conditions defined`, + error: `Invalid condition handle "${sourceHandle}" - no conditions defined`, } } - return validateConditionHandleAgainstBlock(sourceHandle, sourceBlock.id, conditionsValue) + return validateConditionHandle(sourceHandle, sourceBlock.id, conditionsValue) } case 'router': - // Router blocks can have router- prefixed handles or source if (sourceHandle === 'source' || sourceHandle.startsWith('router-')) { return { valid: true } } return { valid: false, - error: `Invalid source handle "${sourceHandle}" for router block. Valid handles are: source, router-{targetId}, error`, + error: `Invalid source handle "${sourceHandle}" for router block. Valid handles: source, router-{targetId}, error`, } default: - // Regular blocks can only have 'source' handle for success paths if (sourceHandle === 'source') { return { valid: true } } return { valid: false, - error: `Invalid source handle "${sourceHandle}" for ${sourceBlockType} block. Valid handles are: source, error`, + error: `Invalid source handle "${sourceHandle}" for ${sourceBlockType} block. Valid handles: source, error`, } } } /** - * Validates that a condition handle references a valid condition in the block - * Handles both semantic keys (if, else-if, else) and internal timestamp-based IDs - * - * Condition handles have the format: condition-{conditionId} - * where conditionId is the full ID from the conditions array (e.g., "blockId-if", "blockId-else-if-timestamp") - * - * For copilot, semantic keys are translated: - * - "if" -> blockId-if - * - "else-if" or "else-if-N" -> matches by position in the conditions array - * - "else" -> blockId-else + * Validates condition handle references a valid condition in the block. + * Accepts both internal IDs (condition-blockId-if) and semantic keys (condition-blockId-else-if) */ -function validateConditionHandleAgainstBlock( +function validateConditionHandle( sourceHandle: string, blockId: string, conditionsValue: string | any[] ): EdgeHandleValidationResult { - // Parse conditions if it's a JSON string let conditions: any[] if (typeof conditionsValue === 'string') { try { conditions = JSON.parse(conditionsValue) } catch { - // Can't parse conditions - this is an error state return { valid: false, - error: `Cannot validate condition handle "${sourceHandle}" - conditions value is not valid JSON`, + error: `Cannot validate condition handle "${sourceHandle}" - conditions is not valid JSON`, } } } else if (Array.isArray(conditionsValue)) { @@ -858,59 +835,42 @@ function validateConditionHandleAgainstBlock( } else { return { valid: false, - error: `Cannot validate condition handle "${sourceHandle}" - conditions value is not an array`, + error: `Cannot validate condition handle "${sourceHandle}" - conditions is not an array`, } } if (!Array.isArray(conditions) || conditions.length === 0) { return { valid: false, - error: `Invalid condition handle "${sourceHandle}" - condition block has no conditions defined`, + error: `Invalid condition handle "${sourceHandle}" - no conditions defined`, } } - // Build set of all valid handles for this condition block const validHandles = new Set() + const semanticPrefix = `condition-${blockId}-` + let elseIfCount = 0 - // Add handles based on actual condition IDs from the block - // Condition IDs are in format: blockId-if, blockId-else-if-timestamp, blockId-else for (const condition of conditions) { if (condition.id) { - // The handle format is: condition-{conditionId} validHandles.add(`condition-${condition.id}`) } - } - // Also add semantic key handles that copilot might use - // These get translated to the actual IDs by createConditionHandle in parsing-utils.ts - // Format: condition-{blockId}-{semanticKey} - const semanticPrefix = `condition-${blockId}-` - - // Map conditions to their semantic keys based on title - let elseIfCount = 0 - for (const condition of conditions) { const title = condition.title?.toLowerCase() if (title === 'if') { validHandles.add(`${semanticPrefix}if`) } else if (title === 'else if') { elseIfCount++ - if (elseIfCount === 1) { - validHandles.add(`${semanticPrefix}else-if`) - } else { - validHandles.add(`${semanticPrefix}else-if-${elseIfCount}`) - } + validHandles.add(elseIfCount === 1 ? `${semanticPrefix}else-if` : `${semanticPrefix}else-if-${elseIfCount}`) } else if (title === 'else') { validHandles.add(`${semanticPrefix}else`) } } - // Check if the provided handle is valid if (validHandles.has(sourceHandle)) { return { valid: true } } - // Build a helpful error message with the valid options - const validOptions = Array.from(validHandles).slice(0, 5) // Show first 5 options + const validOptions = Array.from(validHandles).slice(0, 5) const moreCount = validHandles.size - validOptions.length let validOptionsStr = validOptions.join(', ') if (moreCount > 0) { @@ -919,25 +879,17 @@ function validateConditionHandleAgainstBlock( return { valid: false, - error: `Invalid condition handle "${sourceHandle}" - does not match any condition in the block. Valid handles: ${validOptionsStr}`, + error: `Invalid condition handle "${sourceHandle}". Valid handles: ${validOptionsStr}`, } } /** - * Validates that a target handle is valid - * Currently all blocks accept 'target' as the target handle + * Validates target handle is valid (must be 'target') */ function validateTargetHandle(targetHandle: string): EdgeHandleValidationResult { - // Standard target handle - if (targetHandle === 'target') { - return { valid: true } - } - - // Allow undefined/null which defaults to 'target' - if (!targetHandle) { + if (targetHandle === 'target' || !targetHandle) { return { valid: true } } - return { valid: false, error: `Invalid target handle "${targetHandle}". Expected "target"`, @@ -945,49 +897,116 @@ function validateTargetHandle(targetHandle: string): EdgeHandleValidationResult } /** - * Validates an edge's source and target handles against the workflow state - * Returns validation result with any errors + * Creates a validated edge between two blocks. + * Returns true if edge was created, false if skipped due to validation errors. */ -function validateEdgeHandles( +function createValidatedEdge( + modifiedState: any, sourceBlockId: string, targetBlockId: string, sourceHandle: string, targetHandle: string, - modifiedState: any -): EdgeHandleValidationResult { + operationType: string, + logger: ReturnType, + skippedItems?: SkippedItem[] +): boolean { + if (!modifiedState.blocks[targetBlockId]) { + logger.warn(`Target block "${targetBlockId}" not found. Edge skipped.`, { + sourceBlockId, + targetBlockId, + sourceHandle, + }) + skippedItems?.push({ + type: 'invalid_edge_target', + operationType, + blockId: sourceBlockId, + reason: `Edge from "${sourceBlockId}" to "${targetBlockId}" skipped - target block does not exist`, + details: { sourceHandle, targetHandle, targetId: targetBlockId }, + }) + return false + } + const sourceBlock = modifiedState.blocks[sourceBlockId] if (!sourceBlock) { - return { - valid: false, - error: `Source block "${sourceBlockId}" not found`, - } + logger.warn(`Source block "${sourceBlockId}" not found. Edge skipped.`, { + sourceBlockId, + targetBlockId, + }) + skippedItems?.push({ + type: 'invalid_edge_source', + operationType, + blockId: sourceBlockId, + reason: `Edge from "${sourceBlockId}" to "${targetBlockId}" skipped - source block does not exist`, + details: { sourceHandle, targetHandle, targetId: targetBlockId }, + }) + return false } const sourceBlockType = sourceBlock.type if (!sourceBlockType) { - return { - valid: false, - error: `Source block "${sourceBlockId}" has no type`, - } + logger.warn(`Source block "${sourceBlockId}" has no type. Edge skipped.`, { + sourceBlockId, + targetBlockId, + }) + skippedItems?.push({ + type: 'invalid_edge_source', + operationType, + blockId: sourceBlockId, + reason: `Edge from "${sourceBlockId}" to "${targetBlockId}" skipped - source block has no type`, + details: { sourceHandle, targetHandle, targetId: targetBlockId }, + }) + return false } - // Validate source handle const sourceValidation = validateSourceHandleForBlock(sourceHandle, sourceBlockType, sourceBlock) if (!sourceValidation.valid) { - return sourceValidation + logger.warn(`Invalid source handle. Edge skipped.`, { + sourceBlockId, + targetBlockId, + sourceHandle, + error: sourceValidation.error, + }) + skippedItems?.push({ + type: 'invalid_source_handle', + operationType, + blockId: sourceBlockId, + reason: sourceValidation.error || `Invalid source handle "${sourceHandle}"`, + details: { sourceHandle, targetHandle, targetId: targetBlockId }, + }) + return false } - // Validate target handle const targetValidation = validateTargetHandle(targetHandle) if (!targetValidation.valid) { - return targetValidation + logger.warn(`Invalid target handle. Edge skipped.`, { + sourceBlockId, + targetBlockId, + targetHandle, + error: targetValidation.error, + }) + skippedItems?.push({ + type: 'invalid_target_handle', + operationType, + blockId: sourceBlockId, + reason: targetValidation.error || `Invalid target handle "${targetHandle}"`, + details: { sourceHandle, targetHandle, targetId: targetBlockId }, + }) + return false } - return { valid: true } + modifiedState.edges.push({ + id: crypto.randomUUID(), + source: sourceBlockId, + sourceHandle, + target: targetBlockId, + targetHandle, + type: 'default', + }) + return true } /** - * Helper to add connections as edges for a block + * Adds connections as edges for a block */ function addConnectionsAsEdges( modifiedState: any, @@ -999,66 +1018,16 @@ function addConnectionsAsEdges( Object.entries(connections).forEach(([sourceHandle, targets]) => { const targetArray = Array.isArray(targets) ? targets : [targets] targetArray.forEach((targetId: string) => { - // Validate target block exists - skip edge if target doesn't exist - if (!modifiedState.blocks[targetId]) { - logger.warn( - `Target block "${targetId}" not found when creating connection from "${blockId}". ` + - `Edge skipped.`, - { - sourceBlockId: blockId, - targetBlockId: targetId, - existingBlocks: Object.keys(modifiedState.blocks), - } - ) - skippedItems?.push({ - type: 'invalid_edge_target', - operationType: 'add_edge', - blockId: blockId, - reason: `Edge from "${blockId}" to "${targetId}" skipped - target block does not exist`, - details: { sourceHandle, targetId }, - }) - return - } - - // Validate source and target handles are valid for the block types - const targetHandle = 'target' - const handleValidation = validateEdgeHandles( + createValidatedEdge( + modifiedState, blockId, targetId, sourceHandle, - targetHandle, - modifiedState + 'target', + 'add_edge', + logger, + skippedItems ) - - if (!handleValidation.valid) { - logger.warn( - `Invalid edge handle when creating connection from "${blockId}" to "${targetId}". Edge skipped.`, - { - sourceBlockId: blockId, - targetBlockId: targetId, - sourceHandle, - targetHandle, - error: handleValidation.error, - } - ) - skippedItems?.push({ - type: 'invalid_source_handle', - operationType: 'add_edge', - blockId: blockId, - reason: handleValidation.error || `Invalid handle for edge from "${blockId}" to "${targetId}"`, - details: { sourceHandle, targetHandle, targetId }, - }) - return - } - - modifiedState.edges.push({ - id: crypto.randomUUID(), - source: blockId, - sourceHandle, - target: targetId, - targetHandle, - type: 'default', - }) }) }) } @@ -1541,99 +1510,44 @@ function applyOperationsToWorkflowState( // Handle connections update (convert to edges) if (params?.connections) { - // Remove existing edges from this block modifiedState.edges = modifiedState.edges.filter((edge: any) => edge.source !== block_id) - // Add new edges based on connections Object.entries(params.connections).forEach(([connectionType, targets]) => { if (targets === null) return - // Map semantic connection names to actual React Flow handle IDs - // 'success' in YAML/connections maps to 'source' handle in React Flow const mapConnectionTypeToHandle = (type: string): string => { if (type === 'success') return 'source' if (type === 'error') return 'error' - // Conditions and other types pass through as-is return type } - const actualSourceHandle = mapConnectionTypeToHandle(connectionType) - - const addEdge = (targetBlock: string, targetHandle?: string) => { - // Validate target block exists - skip edge if target doesn't exist - if (!modifiedState.blocks[targetBlock]) { - logger.warn( - `Target block "${targetBlock}" not found when creating connection from "${block_id}". ` + - `Edge skipped.`, - { - sourceBlockId: block_id, - targetBlockId: targetBlock, - existingBlocks: Object.keys(modifiedState.blocks), - } - ) - logSkippedItem(skippedItems, { - type: 'invalid_edge_target', - operationType: 'edit', - blockId: block_id, - reason: `Edge from "${block_id}" to "${targetBlock}" skipped - target block does not exist`, - details: { sourceHandle: actualSourceHandle, targetId: targetBlock }, - }) - return - } + const sourceHandle = mapConnectionTypeToHandle(connectionType) - // Validate source and target handles are valid for the block types - const resolvedTargetHandle = targetHandle || 'target' - const handleValidation = validateEdgeHandles( + const addEdgeForTarget = (targetBlock: string, targetHandle?: string) => { + createValidatedEdge( + modifiedState, block_id, targetBlock, - actualSourceHandle, - resolvedTargetHandle, - modifiedState + sourceHandle, + targetHandle || 'target', + 'edit', + logger, + skippedItems ) - - if (!handleValidation.valid) { - logger.warn( - `Invalid edge handle when creating connection from "${block_id}" to "${targetBlock}". Edge skipped.`, - { - sourceBlockId: block_id, - targetBlockId: targetBlock, - sourceHandle: actualSourceHandle, - targetHandle: resolvedTargetHandle, - error: handleValidation.error, - } - ) - logSkippedItem(skippedItems, { - type: 'invalid_source_handle', - operationType: 'edit', - blockId: block_id, - reason: handleValidation.error || `Invalid handle for edge from "${block_id}" to "${targetBlock}"`, - details: { sourceHandle: actualSourceHandle, targetHandle: resolvedTargetHandle, targetId: targetBlock }, - }) - return - } - - modifiedState.edges.push({ - id: crypto.randomUUID(), - source: block_id, - sourceHandle: actualSourceHandle, - target: targetBlock, - targetHandle: resolvedTargetHandle, - type: 'default', - }) } if (typeof targets === 'string') { - addEdge(targets) + addEdgeForTarget(targets) } else if (Array.isArray(targets)) { targets.forEach((target: any) => { if (typeof target === 'string') { - addEdge(target) + addEdgeForTarget(target) } else if (target?.block) { - addEdge(target.block, target.handle) + addEdgeForTarget(target.block, target.handle) } }) } else if (typeof targets === 'object' && (targets as any)?.block) { - addEdge((targets as any).block, (targets as any).handle) + addEdgeForTarget((targets as any).block, (targets as any).handle) } }) } From a00a85a4c24067f0e9f5ae4c79eb79f0aeb86652 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Thu, 18 Dec 2025 10:56:07 -0800 Subject: [PATCH 3/4] Fix lint --- apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 75daa61e53..6a9b33336a 100644 --- a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts +++ b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts @@ -860,7 +860,9 @@ function validateConditionHandle( validHandles.add(`${semanticPrefix}if`) } else if (title === 'else if') { elseIfCount++ - validHandles.add(elseIfCount === 1 ? `${semanticPrefix}else-if` : `${semanticPrefix}else-if-${elseIfCount}`) + validHandles.add( + elseIfCount === 1 ? `${semanticPrefix}else-if` : `${semanticPrefix}else-if-${elseIfCount}` + ) } else if (title === 'else') { validHandles.add(`${semanticPrefix}else`) } From c47d49c32fb001f852ed52ed190371af86ba6fb6 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Thu, 18 Dec 2025 10:57:27 -0800 Subject: [PATCH 4/4] Fix empty target handle --- apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 6a9b33336a..909f3ee74f 100644 --- a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts +++ b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts @@ -889,7 +889,7 @@ function validateConditionHandle( * Validates target handle is valid (must be 'target') */ function validateTargetHandle(targetHandle: string): EdgeHandleValidationResult { - if (targetHandle === 'target' || !targetHandle) { + if (targetHandle === 'target') { return { valid: true } } return {