diff --git a/README.md b/README.md
index f25393ba18..e93ac2a4bb 100644
--- a/README.md
+++ b/README.md
@@ -9,12 +9,12 @@
-
-
+
+
-
+
### Build Workflows with Ease
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/note-block/note-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/note-block/note-block.tsx
index 31c64f908a..2cfa568165 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/note-block/note-block.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/note-block/note-block.tsx
@@ -168,12 +168,17 @@ const NoteMarkdown = memo(function NoteMarkdown({ content }: { content: string }
)
})
-export const NoteBlock = memo(function NoteBlock({ id, data }: NodeProps) {
+export const NoteBlock = memo(function NoteBlock({
+ id,
+ data,
+ selected,
+}: NodeProps) {
const { type, config, name } = data
const { activeWorkflowId, isEnabled, handleClick, hasRing, ringStyles } = useBlockVisual({
blockId: id,
data,
+ isSelected: selected,
})
const storedValues = useSubBlockStore(
useCallback(
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/subflow-node.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/subflow-node.tsx
index 04e64907fa..3733301c44 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/subflow-node.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/subflow-node.tsx
@@ -66,7 +66,7 @@ export interface SubflowNodeData {
* @param props - Node properties containing data and id
* @returns Rendered subflow node component
*/
-export const SubflowNodeComponent = memo(({ data, id }: NodeProps) => {
+export const SubflowNodeComponent = memo(({ data, id, selected }: NodeProps) => {
const { getNodes } = useReactFlow()
const blockRef = useRef(null)
const userPermissions = useUserPermissionsContext()
@@ -134,13 +134,15 @@ export const SubflowNodeComponent = memo(({ data, id }: NodeProps
{!isPreview && (
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx
index 4559467522..8c8897fbd3 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx
@@ -208,7 +208,6 @@ const tryParseJson = (value: unknown): unknown => {
export const getDisplayValue = (value: unknown): string => {
if (value == null || value === '') return '-'
- // Try parsing JSON strings first
const parsedValue = tryParseJson(value)
if (isMessagesArray(parsedValue)) {
@@ -557,6 +556,7 @@ const SubBlockRow = ({
export const WorkflowBlock = memo(function WorkflowBlock({
id,
data,
+ selected,
}: NodeProps) {
const { type, config, name, isPending } = data
@@ -574,7 +574,7 @@ export const WorkflowBlock = memo(function WorkflowBlock({
hasRing,
ringStyles,
runPathStatus,
- } = useBlockVisual({ blockId: id, data, isPending })
+ } = useBlockVisual({ blockId: id, data, isPending, isSelected: selected })
const currentBlock = currentWorkflow.getBlockById(id)
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-visual.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-visual.ts
index 20246d97d2..33f83bdb94 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-visual.ts
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-visual.ts
@@ -17,6 +17,8 @@ interface UseBlockVisualProps {
data: WorkflowBlockProps
/** Whether the block is pending execution */
isPending?: boolean
+ /** Whether the block is selected (via shift-click or selection box) */
+ isSelected?: boolean
}
/**
@@ -28,7 +30,12 @@ interface UseBlockVisualProps {
* @param props - The hook properties
* @returns Visual state, click handler, and ring styling for the block
*/
-export function useBlockVisual({ blockId, data, isPending = false }: UseBlockVisualProps) {
+export function useBlockVisual({
+ blockId,
+ data,
+ isPending = false,
+ isSelected = false,
+}: UseBlockVisualProps) {
const isPreview = data.isPreview ?? false
const isPreviewSelected = data.isPreviewSelected ?? false
@@ -42,7 +49,6 @@ export function useBlockVisual({ blockId, data, isPending = false }: UseBlockVis
isDeletedBlock,
} = useBlockState(blockId, currentWorkflow, data)
- // Check if the editor panel is open for this block
const currentBlockId = usePanelEditorStore((state) => state.currentBlockId)
const activeTab = usePanelStore((state) => state.activeTab)
const isEditorOpen = !isPreview && currentBlockId === blockId && activeTab === 'editor'
@@ -68,6 +74,7 @@ export function useBlockVisual({ blockId, data, isPending = false }: UseBlockVis
diffStatus: isPreview ? undefined : diffStatus,
runPathStatus,
isPreviewSelection: isPreview && isPreviewSelected,
+ isSelected: isPreview ? false : isSelected,
}),
[
isExecuting,
@@ -78,6 +85,7 @@ export function useBlockVisual({ blockId, data, isPending = false }: UseBlockVis
runPathStatus,
isPreview,
isPreviewSelected,
+ isSelected,
]
)
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/block-ring-utils.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/block-ring-utils.ts
index 19eec88625..62c178337b 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/block-ring-utils.ts
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/block-ring-utils.ts
@@ -14,6 +14,8 @@ export interface BlockRingOptions {
diffStatus: BlockDiffStatus
runPathStatus: BlockRunPathStatus
isPreviewSelection?: boolean
+ /** Whether the block is selected via shift-click or selection box (shows blue ring) */
+ isSelected?: boolean
}
/**
@@ -32,11 +34,13 @@ export function getBlockRingStyles(options: BlockRingOptions): {
diffStatus,
runPathStatus,
isPreviewSelection,
+ isSelected,
} = options
const hasRing =
isExecuting ||
isEditorOpen ||
+ isSelected ||
isPending ||
diffStatus === 'new' ||
diffStatus === 'edited' ||
@@ -46,25 +50,37 @@ export function getBlockRingStyles(options: BlockRingOptions): {
const ringClassName = cn(
// Executing block: pulsing success ring with prominent thickness (highest priority)
isExecuting && 'ring-[3.5px] ring-[var(--border-success)] animate-ring-pulse',
- // Editor open or preview selection: static blue ring
+ // Editor open, selected, or preview selection: static blue ring
!isExecuting &&
- (isEditorOpen || isPreviewSelection) &&
+ (isEditorOpen || isSelected || isPreviewSelection) &&
'ring-[1.75px] ring-[var(--brand-secondary)]',
// Non-active states use standard ring utilities
- !isExecuting && !isEditorOpen && !isPreviewSelection && hasRing && 'ring-[1.75px]',
+ !isExecuting &&
+ !isEditorOpen &&
+ !isSelected &&
+ !isPreviewSelection &&
+ hasRing &&
+ 'ring-[1.75px]',
// Pending state: warning ring
- !isExecuting && !isEditorOpen && isPending && 'ring-[var(--warning)]',
+ !isExecuting && !isEditorOpen && !isSelected && isPending && 'ring-[var(--warning)]',
// Deleted state (highest priority after active/pending)
- !isExecuting && !isEditorOpen && !isPending && isDeletedBlock && 'ring-[var(--text-error)]',
+ !isExecuting &&
+ !isEditorOpen &&
+ !isSelected &&
+ !isPending &&
+ isDeletedBlock &&
+ 'ring-[var(--text-error)]',
// Diff states
!isExecuting &&
!isEditorOpen &&
+ !isSelected &&
!isPending &&
!isDeletedBlock &&
diffStatus === 'new' &&
'ring-[var(--brand-tertiary-2)]',
!isExecuting &&
!isEditorOpen &&
+ !isSelected &&
!isPending &&
!isDeletedBlock &&
diffStatus === 'edited' &&
@@ -72,6 +88,7 @@ export function getBlockRingStyles(options: BlockRingOptions): {
// Run path states (lowest priority - only show if no other states active)
!isExecuting &&
!isEditorOpen &&
+ !isSelected &&
!isPending &&
!isDeletedBlock &&
!diffStatus &&
@@ -79,6 +96,7 @@ export function getBlockRingStyles(options: BlockRingOptions): {
'ring-[var(--border-success)]',
!isExecuting &&
!isEditorOpen &&
+ !isSelected &&
!isPending &&
!isDeletedBlock &&
!diffStatus &&
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx
index ec6274a10e..f2a0db4d7a 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx
@@ -700,7 +700,23 @@ const WorkflowContent = React.memo(() => {
triggerMode,
})
- collaborativeBatchAddBlocks([block], autoConnectEdge ? [autoConnectEdge] : [], {}, {}, {})
+ const subBlockValues: Record> = {}
+ if (block.subBlocks && Object.keys(block.subBlocks).length > 0) {
+ subBlockValues[id] = {}
+ for (const [subBlockId, subBlock] of Object.entries(block.subBlocks)) {
+ if (subBlock.value !== null && subBlock.value !== undefined) {
+ subBlockValues[id][subBlockId] = subBlock.value
+ }
+ }
+ }
+
+ collaborativeBatchAddBlocks(
+ [block],
+ autoConnectEdge ? [autoConnectEdge] : [],
+ {},
+ {},
+ subBlockValues
+ )
usePanelEditorStore.getState().setCurrentBlockId(id)
},
[collaborativeBatchAddBlocks, setSelectedEdges]
diff --git a/apps/sim/app/workspace/providers/socket-provider.tsx b/apps/sim/app/workspace/providers/socket-provider.tsx
index f173864273..434af6891b 100644
--- a/apps/sim/app/workspace/providers/socket-provider.tsx
+++ b/apps/sim/app/workspace/providers/socket-provider.tsx
@@ -406,21 +406,13 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
socketInstance.on('cursor-update', (data) => {
setPresenceUsers((prev) => {
const existingIndex = prev.findIndex((user) => user.socketId === data.socketId)
- if (existingIndex !== -1) {
- return prev.map((user) =>
- user.socketId === data.socketId ? { ...user, cursor: data.cursor } : user
- )
+ if (existingIndex === -1) {
+ logger.debug('Received cursor-update for unknown user', { socketId: data.socketId })
+ return prev
}
- return [
- ...prev,
- {
- socketId: data.socketId,
- userId: data.userId,
- userName: data.userName,
- avatarUrl: data.avatarUrl,
- cursor: data.cursor,
- },
- ]
+ return prev.map((user) =>
+ user.socketId === data.socketId ? { ...user, cursor: data.cursor } : user
+ )
})
eventHandlers.current.cursorUpdate?.(data)
})
@@ -428,21 +420,15 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
socketInstance.on('selection-update', (data) => {
setPresenceUsers((prev) => {
const existingIndex = prev.findIndex((user) => user.socketId === data.socketId)
- if (existingIndex !== -1) {
- return prev.map((user) =>
- user.socketId === data.socketId ? { ...user, selection: data.selection } : user
- )
- }
- return [
- ...prev,
- {
+ if (existingIndex === -1) {
+ logger.debug('Received selection-update for unknown user', {
socketId: data.socketId,
- userId: data.userId,
- userName: data.userName,
- avatarUrl: data.avatarUrl,
- selection: data.selection,
- },
- ]
+ })
+ return prev
+ }
+ return prev.map((user) =>
+ user.socketId === data.socketId ? { ...user, selection: data.selection } : user
+ )
})
eventHandlers.current.selectionUpdate?.(data)
})
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 a17af3cd3a..fb1598fc20 100644
--- a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts
+++ b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts
@@ -2499,7 +2499,9 @@ export const editWorkflowServerTool: BaseServerTool = {
async execute(params: EditWorkflowParams, context?: { userId: string }): Promise {
const logger = createLogger('EditWorkflowServerTool')
const { operations, workflowId, currentUserWorkflow } = params
- if (!operations || operations.length === 0) throw new Error('operations are required')
+ if (!Array.isArray(operations) || operations.length === 0) {
+ throw new Error('operations are required and must be an array')
+ }
if (!workflowId) throw new Error('workflowId is required')
logger.info('Executing edit_workflow', {
diff --git a/apps/sim/lib/workflows/persistence/utils.ts b/apps/sim/lib/workflows/persistence/utils.ts
index c27fefcf6a..d3b26e4ea6 100644
--- a/apps/sim/lib/workflows/persistence/utils.ts
+++ b/apps/sim/lib/workflows/persistence/utils.ts
@@ -1,7 +1,6 @@
import crypto from 'crypto'
import {
db,
- webhook,
workflow,
workflowBlocks,
workflowDeploymentVersion,
@@ -22,7 +21,6 @@ import { generateLoopBlocks, generateParallelBlocks } from '@/stores/workflows/w
const logger = createLogger('WorkflowDBHelpers')
export type WorkflowDeploymentVersion = InferSelectModel
-type WebhookRecord = InferSelectModel
type SubflowInsert = InferInsertModel
export interface WorkflowDeploymentVersionResponse {
@@ -337,18 +335,6 @@ export async function saveWorkflowToNormalizedTables(
// Start a transaction
await db.transaction(async (tx) => {
- // Snapshot existing webhooks before deletion to preserve them through the cycle
- let existingWebhooks: WebhookRecord[] = []
- try {
- existingWebhooks = await tx.select().from(webhook).where(eq(webhook.workflowId, workflowId))
- } catch (webhookError) {
- // Webhook table might not be available in test environments
- logger.debug('Could not load webhooks before save, skipping preservation', {
- error: webhookError instanceof Error ? webhookError.message : String(webhookError),
- })
- }
-
- // Clear existing data for this workflow
await Promise.all([
tx.delete(workflowBlocks).where(eq(workflowBlocks.workflowId, workflowId)),
tx.delete(workflowEdges).where(eq(workflowEdges.workflowId, workflowId)),
@@ -419,42 +405,6 @@ export async function saveWorkflowToNormalizedTables(
if (subflowInserts.length > 0) {
await tx.insert(workflowSubflows).values(subflowInserts)
}
-
- // Re-insert preserved webhooks if any exist and their blocks still exist
- if (existingWebhooks.length > 0) {
- try {
- const webhookInserts = existingWebhooks
- .filter((wh) => !!state.blocks?.[wh.blockId ?? ''])
- .map((wh) => ({
- id: wh.id,
- workflowId: wh.workflowId,
- blockId: wh.blockId,
- path: wh.path,
- provider: wh.provider,
- providerConfig: wh.providerConfig,
- credentialSetId: wh.credentialSetId,
- isActive: wh.isActive,
- createdAt: wh.createdAt,
- updatedAt: new Date(),
- }))
-
- if (webhookInserts.length > 0) {
- await tx.insert(webhook).values(webhookInserts)
- logger.debug(`Preserved ${webhookInserts.length} webhook(s) through workflow save`, {
- workflowId,
- })
- }
- } catch (webhookInsertError) {
- // Webhook preservation is optional - don't fail the entire save if it errors
- logger.warn('Could not preserve webhooks during save', {
- error:
- webhookInsertError instanceof Error
- ? webhookInsertError.message
- : String(webhookInsertError),
- workflowId,
- })
- }
- }
})
return { success: true }
diff --git a/apps/sim/socket/database/operations.ts b/apps/sim/socket/database/operations.ts
index 3f30bea034..5fa69f8d98 100644
--- a/apps/sim/socket/database/operations.ts
+++ b/apps/sim/socket/database/operations.ts
@@ -1,7 +1,6 @@
import * as schema from '@sim/db'
import { webhook, workflow, workflowBlocks, workflowEdges, workflowSubflows } from '@sim/db'
import { createLogger } from '@sim/logger'
-import type { InferSelectModel } from 'drizzle-orm'
import { and, eq, inArray, or, sql } from 'drizzle-orm'
import { drizzle } from 'drizzle-orm/postgres-js'
import postgres from 'postgres'
@@ -1175,14 +1174,6 @@ async function handleWorkflowOperationTx(
parallelCount: Object.keys(parallels || {}).length,
})
- // Snapshot existing webhooks before deletion to preserve them through the cycle
- // (workflowBlocks has CASCADE DELETE to webhook table)
- const existingWebhooks = await tx
- .select()
- .from(webhook)
- .where(eq(webhook.workflowId, workflowId))
-
- // Delete all existing blocks (this will cascade delete edges and webhooks via ON DELETE CASCADE)
await tx.delete(workflowBlocks).where(eq(workflowBlocks.workflowId, workflowId))
// Delete all existing subflows
@@ -1248,32 +1239,6 @@ async function handleWorkflowOperationTx(
await tx.insert(workflowSubflows).values(parallelValues)
}
- // Re-insert preserved webhooks if any exist and their blocks still exist
- type WebhookRecord = InferSelectModel
- if (existingWebhooks.length > 0) {
- const webhookInserts = existingWebhooks
- .filter((wh: WebhookRecord) => !!blocks?.[wh.blockId ?? ''])
- .map((wh: WebhookRecord) => ({
- id: wh.id,
- workflowId: wh.workflowId,
- blockId: wh.blockId,
- path: wh.path,
- provider: wh.provider,
- providerConfig: wh.providerConfig,
- credentialSetId: wh.credentialSetId,
- isActive: wh.isActive,
- createdAt: wh.createdAt,
- updatedAt: new Date(),
- }))
-
- if (webhookInserts.length > 0) {
- await tx.insert(webhook).values(webhookInserts)
- logger.debug(`Preserved ${webhookInserts.length} webhook(s) through state replacement`, {
- workflowId,
- })
- }
- }
-
logger.info(`Successfully replaced workflow state for ${workflowId}`)
break
}