Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 16 additions & 15 deletions apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(() =>
Expand Down Expand Up @@ -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])

Expand Down Expand Up @@ -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 => {
Expand All @@ -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

Expand Down Expand Up @@ -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. */
Expand Down Expand Up @@ -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) {
Expand All @@ -1225,7 +1232,7 @@ const WorkflowContent = React.memo(() => {
}
: undefined
},
[]
[blocks, isAutoConnectSourceCandidate]
)

/**
Expand Down Expand Up @@ -2364,21 +2371,15 @@ 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)
) {
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
}

Expand Down
48 changes: 42 additions & 6 deletions apps/sim/stores/workflows/workflow/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -90,6 +91,38 @@ function resolveInitialSubblockValue(config: SubBlockConfig): unknown {
return null
}

function isValidEdge(
edge: Edge,
blocks: Record<string, { type: string; triggerMode?: boolean }>
): 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<string, { type: string; triggerMode?: boolean }>
): 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: [],
Expand Down Expand Up @@ -360,8 +393,9 @@ export const useWorkflowStore = create<WorkflowStore>()(
}

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(),
Expand Down Expand Up @@ -495,8 +529,11 @@ export const useWorkflowStore = create<WorkflowStore>()(
},

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) {
Expand All @@ -512,7 +549,6 @@ export const useWorkflowStore = create<WorkflowStore>()(
})
}

const blocks = get().blocks
set({
blocks: { ...blocks },
edges: newEdges,
Expand Down Expand Up @@ -572,7 +608,7 @@ export const useWorkflowStore = create<WorkflowStore>()(
) => {
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
Expand Down Expand Up @@ -1083,7 +1119,7 @@ export const useWorkflowStore = create<WorkflowStore>()(

const newState = {
blocks: deployedState.blocks,
edges: deployedState.edges,
edges: filterValidEdges(deployedState.edges ?? [], deployedState.blocks),
loops: deployedState.loops || {},
parallels: deployedState.parallels || {},
needsRedeployment: false,
Expand Down