From bd2838a88c081e4757d83994138bb9222f894f48 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Sat, 24 Jan 2026 02:44:52 -0800 Subject: [PATCH 1/5] fix(notes): ghost edges --- .../[workspaceId]/w/[workflowId]/workflow.tsx | 31 ++++++------ apps/sim/stores/workflows/workflow/store.ts | 48 ++++++++++++++++--- 2 files changed, 58 insertions(+), 21 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx index a1089e0a76..7c42fd4b12 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx @@ -81,6 +81,7 @@ import { useWorkflowDiffStore } from '@/stores/workflow-diff/store' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' import { getUniqueBlockName, prepareBlockState } from '@/stores/workflows/utils' import { useWorkflowStore } from '@/stores/workflows/workflow/store' +import type { BlockState } from '@/stores/workflows/workflow/types' /** Lazy-loaded components for non-critical UI that can load after initial render */ const LazyChat = lazy(() => @@ -535,8 +536,7 @@ const WorkflowContent = React.memo(() => { return edgesToFilter.filter((edge) => { const sourceBlock = blocks[edge.source] const targetBlock = blocks[edge.target] - if (!sourceBlock || !targetBlock) return false - return !isAnnotationOnlyBlock(sourceBlock.type) && !isAnnotationOnlyBlock(targetBlock.type) + return Boolean(sourceBlock && targetBlock) }) }, [edges, isShowingDiff, isDiffReady, diffAnalysis, blocks]) @@ -1097,6 +1097,13 @@ const WorkflowContent = React.memo(() => { [collaborativeBatchRemoveEdges] ) + const isAutoConnectSourceCandidate = useCallback((block: BlockState): boolean => { + if (!block.enabled) return false + if (block.type === 'response') return false + if (isAnnotationOnlyBlock(block.type)) return false + return true + }, []) + /** Finds the closest block to a position for auto-connect. */ const findClosestOutput = useCallback( (newNodePosition: { x: number; y: number }): BlockData | null => { @@ -1109,8 +1116,7 @@ const WorkflowContent = React.memo(() => { position: { x: number; y: number } distanceSquared: number } | null>((acc, [id, block]) => { - if (!block.enabled) return acc - if (block.type === 'response') return acc + if (!isAutoConnectSourceCandidate(block)) return acc const node = nodeIndex.get(id) if (!node) return acc @@ -1140,7 +1146,7 @@ const WorkflowContent = React.memo(() => { position: closest.position, } }, - [blocks, getNodes, getNodeAnchorPosition, isPointInLoopNode] + [blocks, getNodes, getNodeAnchorPosition, isPointInLoopNode, isAutoConnectSourceCandidate] ) /** Determines the appropriate source handle based on block type. */ @@ -1208,7 +1214,8 @@ const WorkflowContent = React.memo(() => { position: { x: number; y: number } distanceSquared: number } | null>((acc, block) => { - if (block.type === 'response') return acc + const blockState = blocks[block.id] + if (!blockState || !isAutoConnectSourceCandidate(blockState)) return acc const distanceSquared = (block.position.x - targetPosition.x) ** 2 + (block.position.y - targetPosition.y) ** 2 if (!acc || distanceSquared < acc.distanceSquared) { @@ -1225,7 +1232,7 @@ const WorkflowContent = React.memo(() => { } : undefined }, - [] + [blocks, isAutoConnectSourceCandidate] ) /** @@ -2364,7 +2371,6 @@ const WorkflowContent = React.memo(() => { if (!sourceNode || !targetNode) return - // Prevent connections to/from annotation-only blocks (non-executable) if ( isAnnotationOnlyBlock(sourceNode.data?.type) || isAnnotationOnlyBlock(targetNode.data?.type) @@ -2372,13 +2378,8 @@ const WorkflowContent = React.memo(() => { return } - // Prevent incoming connections to trigger blocks (webhook, schedule, etc.) - if (targetNode.data?.config?.category === 'triggers') { - return - } - - // Prevent incoming connections to starter blocks (still keep separate for backward compatibility) - if (targetNode.data?.type === 'starter') { + const targetBlock = blocks[targetNode.id] + if (targetBlock && TriggerUtils.isTriggerBlock(targetBlock)) { return } diff --git a/apps/sim/stores/workflows/workflow/store.ts b/apps/sim/stores/workflows/workflow/store.ts index 3fc3438cf3..bdb8f86130 100644 --- a/apps/sim/stores/workflows/workflow/store.ts +++ b/apps/sim/stores/workflows/workflow/store.ts @@ -4,9 +4,10 @@ import { create } from 'zustand' import { devtools } from 'zustand/middleware' import { DEFAULT_DUPLICATE_OFFSET } from '@/lib/workflows/autolayout/constants' import { getBlockOutputs } from '@/lib/workflows/blocks/block-outputs' +import { TriggerUtils } from '@/lib/workflows/triggers/triggers' import { getBlock } from '@/blocks' import type { SubBlockConfig } from '@/blocks/types' -import { normalizeName, RESERVED_BLOCK_NAMES } from '@/executor/constants' +import { isAnnotationOnlyBlock, normalizeName, RESERVED_BLOCK_NAMES } from '@/executor/constants' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' import { useSubBlockStore } from '@/stores/workflows/subblock/store' import { filterNewEdges, getUniqueBlockName, mergeSubblockState } from '@/stores/workflows/utils' @@ -90,6 +91,38 @@ function resolveInitialSubblockValue(config: SubBlockConfig): unknown { return null } +function isValidEdge( + edge: Edge, + blocks: Record +): boolean { + const sourceBlock = blocks[edge.source] + const targetBlock = blocks[edge.target] + if (!sourceBlock || !targetBlock) return false + if (isAnnotationOnlyBlock(sourceBlock.type)) return false + if (isAnnotationOnlyBlock(targetBlock.type)) return false + if (TriggerUtils.isTriggerBlock(targetBlock)) return false + return true +} + +function filterValidEdges( + edges: Edge[], + blocks: Record +): Edge[] { + return edges.filter((edge) => { + const valid = isValidEdge(edge, blocks) + if (!valid) { + logger.warn('Filtered invalid edge', { + edgeId: edge.id, + source: edge.source, + target: edge.target, + sourceType: blocks[edge.source]?.type, + targetType: blocks[edge.target]?.type, + }) + } + return valid + }) +} + const initialState = { blocks: {}, edges: [], @@ -360,8 +393,9 @@ export const useWorkflowStore = create()( } if (edges && edges.length > 0) { + const validEdges = filterValidEdges(edges, newBlocks) const existingEdgeIds = new Set(currentEdges.map((e) => e.id)) - for (const edge of edges) { + for (const edge of validEdges) { if (!existingEdgeIds.has(edge.id)) { newEdges.push({ id: edge.id || crypto.randomUUID(), @@ -495,8 +529,11 @@ export const useWorkflowStore = create()( }, batchAddEdges: (edges: Edge[]) => { + const blocks = get().blocks const currentEdges = get().edges - const filtered = filterNewEdges(edges, currentEdges) + + const validEdges = filterValidEdges(edges, blocks) + const filtered = filterNewEdges(validEdges, currentEdges) const newEdges = [...currentEdges] for (const edge of filtered) { @@ -512,7 +549,6 @@ export const useWorkflowStore = create()( }) } - const blocks = get().blocks set({ blocks: { ...blocks }, edges: newEdges, @@ -572,7 +608,7 @@ export const useWorkflowStore = create()( ) => { set((state) => { const nextBlocks = workflowState.blocks || {} - const nextEdges = workflowState.edges || [] + const nextEdges = filterValidEdges(workflowState.edges || [], nextBlocks) const nextLoops = Object.keys(workflowState.loops || {}).length > 0 ? workflowState.loops @@ -1083,7 +1119,7 @@ export const useWorkflowStore = create()( const newState = { blocks: deployedState.blocks, - edges: deployedState.edges, + edges: filterValidEdges(deployedState.edges || [], deployedState.blocks || {}), loops: deployedState.loops || {}, parallels: deployedState.parallels || {}, needsRedeployment: false, From 4cda2b4eba9a7bdcfb03b63bba510d05ca951e5f Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Sat, 24 Jan 2026 02:49:37 -0800 Subject: [PATCH 2/5] fix deployed state fallback --- apps/sim/stores/workflows/workflow/store.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sim/stores/workflows/workflow/store.ts b/apps/sim/stores/workflows/workflow/store.ts index bdb8f86130..bbcd7cf79d 100644 --- a/apps/sim/stores/workflows/workflow/store.ts +++ b/apps/sim/stores/workflows/workflow/store.ts @@ -1119,7 +1119,7 @@ export const useWorkflowStore = create()( const newState = { blocks: deployedState.blocks, - edges: filterValidEdges(deployedState.edges || [], deployedState.blocks || {}), + edges: filterValidEdges(deployedState.edges, deployedState.blocks), loops: deployedState.loops || {}, parallels: deployedState.parallels || {}, needsRedeployment: false, From 8d751359c3bdcc15d24bb5ec68ff7efcc6e03d62 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Sat, 24 Jan 2026 03:21:48 -0800 Subject: [PATCH 3/5] fallback --- apps/sim/stores/workflows/workflow/store.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sim/stores/workflows/workflow/store.ts b/apps/sim/stores/workflows/workflow/store.ts index bbcd7cf79d..12215fe3dc 100644 --- a/apps/sim/stores/workflows/workflow/store.ts +++ b/apps/sim/stores/workflows/workflow/store.ts @@ -1119,7 +1119,7 @@ export const useWorkflowStore = create()( const newState = { blocks: deployedState.blocks, - edges: filterValidEdges(deployedState.edges, deployedState.blocks), + edges: filterValidEdges(deployedState.edges ?? [], deployedState.blocks), loops: deployedState.loops || {}, parallels: deployedState.parallels || {}, needsRedeployment: false, From 89f4c71acc41c5715f60accb609224622a671122 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Sat, 24 Jan 2026 10:51:12 -0800 Subject: [PATCH 4/5] remove UI level checks --- .../[workspaceId]/w/[workflowId]/workflow.tsx | 36 ------------------- .../workspace/providers/socket-provider.tsx | 2 +- apps/sim/stores/workflows/workflow/store.ts | 14 +------- 3 files changed, 2 insertions(+), 50 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx index 7c42fd4b12..e67a6fe4a0 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx @@ -61,7 +61,6 @@ import { } from '@/app/workspace/[workspaceId]/w/[workflowId]/utils' import { useSocket } from '@/app/workspace/providers/socket-provider' import { getBlock } from '@/blocks' -import { isAnnotationOnlyBlock } from '@/executor/constants' import { useWorkspaceEnvironment } from '@/hooks/queries/environment' import { useAutoConnect, useSnapToGridSize } from '@/hooks/queries/general-settings' import { useCanvasViewport } from '@/hooks/use-canvas-viewport' @@ -1100,7 +1099,6 @@ const WorkflowContent = React.memo(() => { const isAutoConnectSourceCandidate = useCallback((block: BlockState): boolean => { if (!block.enabled) return false if (block.type === 'response') return false - if (isAnnotationOnlyBlock(block.type)) return false return true }, []) @@ -1248,8 +1246,6 @@ const WorkflowContent = React.memo(() => { position: { x: number; y: number }, targetBlockId: string, options: { - blockType: string - enableTriggerMode?: boolean targetParentId?: string | null existingChildBlocks?: { id: string; type: string; position: { x: number; y: number } }[] containerId?: string @@ -1257,17 +1253,6 @@ const WorkflowContent = React.memo(() => { ): Edge | undefined => { if (!autoConnectRef.current) return undefined - // Don't auto-connect starter or annotation-only blocks - if (options.blockType === 'starter' || isAnnotationOnlyBlock(options.blockType)) { - return undefined - } - - // Check if target is a trigger block - const targetBlockConfig = getBlock(options.blockType) - const isTargetTrigger = - options.enableTriggerMode || targetBlockConfig?.category === 'triggers' - if (isTargetTrigger) return undefined - // Case 1: Adding block inside a container with existing children if (options.existingChildBlocks && options.existingChildBlocks.length > 0) { const closestBlock = findClosestBlockInSet(options.existingChildBlocks, position) @@ -1375,7 +1360,6 @@ const WorkflowContent = React.memo(() => { const name = getUniqueBlockName(baseName, blocks) const autoConnectEdge = tryCreateAutoConnectEdge(position, id, { - blockType: data.type, targetParentId: null, }) @@ -1446,8 +1430,6 @@ const WorkflowContent = React.memo(() => { .map((b) => ({ id: b.id, type: b.type, position: b.position })) const autoConnectEdge = tryCreateAutoConnectEdge(relativePosition, id, { - blockType: data.type, - enableTriggerMode: data.enableTriggerMode, targetParentId: containerInfo.loopId, existingChildBlocks, containerId: containerInfo.loopId, @@ -1476,8 +1458,6 @@ const WorkflowContent = React.memo(() => { if (checkTriggerConstraints(data.type)) return const autoConnectEdge = tryCreateAutoConnectEdge(position, id, { - blockType: data.type, - enableTriggerMode: data.enableTriggerMode, targetParentId: null, }) @@ -1533,7 +1513,6 @@ const WorkflowContent = React.memo(() => { const name = getUniqueBlockName(baseName, blocks) const autoConnectEdge = tryCreateAutoConnectEdge(basePosition, id, { - blockType: type, targetParentId: null, }) @@ -1569,8 +1548,6 @@ const WorkflowContent = React.memo(() => { const name = getUniqueBlockName(baseName, blocks) const autoConnectEdge = tryCreateAutoConnectEdge(basePosition, id, { - blockType: type, - enableTriggerMode, targetParentId: null, }) @@ -2371,18 +2348,6 @@ const WorkflowContent = React.memo(() => { if (!sourceNode || !targetNode) return - if ( - isAnnotationOnlyBlock(sourceNode.data?.type) || - isAnnotationOnlyBlock(targetNode.data?.type) - ) { - return - } - - const targetBlock = blocks[targetNode.id] - if (targetBlock && TriggerUtils.isTriggerBlock(targetBlock)) { - return - } - // Get parent information (handle container start node case) const sourceParentId = blocks[sourceNode.id]?.data?.parentId || @@ -2788,7 +2753,6 @@ const WorkflowContent = React.memo(() => { .map((b) => ({ id: b.id, type: b.type, position: b.position })) const autoConnectEdge = tryCreateAutoConnectEdge(relativePositionBefore, node.id, { - blockType: node.data?.type || '', targetParentId: potentialParentId, existingChildBlocks, containerId: potentialParentId, diff --git a/apps/sim/app/workspace/providers/socket-provider.tsx b/apps/sim/app/workspace/providers/socket-provider.tsx index c4753b384c..4e18d14b13 100644 --- a/apps/sim/app/workspace/providers/socket-provider.tsx +++ b/apps/sim/app/workspace/providers/socket-provider.tsx @@ -352,7 +352,7 @@ export function SocketProvider({ children, user }: SocketProviderProps) { }) }) - useWorkflowStore.setState({ + useWorkflowStore.getState().replaceWorkflowState({ blocks: workflowState.blocks || {}, edges: workflowState.edges || [], loops: workflowState.loops || {}, diff --git a/apps/sim/stores/workflows/workflow/store.ts b/apps/sim/stores/workflows/workflow/store.ts index 12215fe3dc..763fd7a4a2 100644 --- a/apps/sim/stores/workflows/workflow/store.ts +++ b/apps/sim/stores/workflows/workflow/store.ts @@ -108,19 +108,7 @@ function filterValidEdges( edges: Edge[], blocks: Record ): Edge[] { - return edges.filter((edge) => { - const valid = isValidEdge(edge, blocks) - if (!valid) { - logger.warn('Filtered invalid edge', { - edgeId: edge.id, - source: edge.source, - target: edge.target, - sourceType: blocks[edge.source]?.type, - targetType: blocks[edge.target]?.type, - }) - } - return valid - }) + return edges.filter((edge) => isValidEdge(edge, blocks)) } const initialState = { From c56df9cfb062f2cd1e617b6e65f641c3dc808e54 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Sat, 24 Jan 2026 11:01:31 -0800 Subject: [PATCH 5/5] annotation missing from autoconnect source check --- .../sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx index e67a6fe4a0..77a1b7953e 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx @@ -61,6 +61,7 @@ import { } from '@/app/workspace/[workspaceId]/w/[workflowId]/utils' import { useSocket } from '@/app/workspace/providers/socket-provider' import { getBlock } from '@/blocks' +import { isAnnotationOnlyBlock } from '@/executor/constants' import { useWorkspaceEnvironment } from '@/hooks/queries/environment' import { useAutoConnect, useSnapToGridSize } from '@/hooks/queries/general-settings' import { useCanvasViewport } from '@/hooks/use-canvas-viewport' @@ -1099,6 +1100,7 @@ const WorkflowContent = React.memo(() => { const isAutoConnectSourceCandidate = useCallback((block: BlockState): boolean => { if (!block.enabled) return false if (block.type === 'response') return false + if (isAnnotationOnlyBlock(block.type)) return false return true }, [])