Compare commits

..

28 Commits

Author SHA1 Message Date
Waleed
af82820a28 v0.5.61: webhook improvements, workflow controls, react query for deployment status, chat fixes, reducto and pulse OCR, linear fixes 2026-01-16 18:06:23 -08:00
Waleed
4372841797 v0.5.60: invitation flow improvements, chat fixes, a2a improvements, additional copilot actions 2026-01-15 00:02:18 -08:00
Waleed
5e8c843241 v0.5.59: a2a support, documentation 2026-01-13 13:21:21 -08:00
Waleed
7bf3d73ee6 v0.5.58: export folders, new tools, permissions groups enhancements 2026-01-13 00:56:59 -08:00
Vikhyath Mondreti
7ffc11a738 v0.5.57: subagents, context menu improvements, bug fixes 2026-01-11 11:38:40 -08:00
Waleed
be578e2ed7 v0.5.56: batch operations, access control and permission groups, billing fixes 2026-01-10 00:31:34 -08:00
Waleed
f415e5edc4 v0.5.55: polling groups, bedrock provider, devcontainer fixes, workflow preview enhancements 2026-01-08 23:36:56 -08:00
Waleed
13a6e6c3fa v0.5.54: seo, model blacklist, helm chart updates, fireflies integration, autoconnect improvements, billing fixes 2026-01-07 16:09:45 -08:00
Waleed
f5ab7f21ae v0.5.53: hotkey improvements, added redis fallback, fixes for workflow tool 2026-01-06 23:34:52 -08:00
Waleed
bfb6fffe38 v0.5.52: new port-based router block, combobox expression and variable support 2026-01-06 16:14:10 -08:00
Waleed
4fbec0a43f v0.5.51: triggers, kb, condition block improvements, supabase and grain integration updates 2026-01-06 14:26:46 -08:00
Waleed
585f5e365b v0.5.50: import improvements, ui upgrades, kb styling and performance improvements 2026-01-05 00:35:55 -08:00
Waleed
3792bdd252 v0.5.49: hitl improvements, new email styles, imap trigger, logs context menu (#2672)
* feat(logs-context-menu): consolidated logs utils and types, added logs record context menu (#2659)

* feat(email): welcome email; improvement(emails): ui/ux (#2658)

* feat(email): welcome email; improvement(emails): ui/ux

* improvement(emails): links, accounts, preview

* refactor(emails): file structure and wrapper components

* added envvar for personal emails sent, added isHosted gate

* fixed failing tests, added env mock

* fix: removed comment

---------

Co-authored-by: waleed <walif6@gmail.com>

* fix(logging): hitl + trigger dev crash protection (#2664)

* hitl gaps

* deal with trigger worker crashes

* cleanup import strcuture

* feat(imap): added support for imap trigger (#2663)

* feat(tools): added support for imap trigger

* feat(imap): added parity, tested

* ack PR comments

* final cleanup

* feat(i18n): update translations (#2665)

Co-authored-by: waleedlatif1 <waleedlatif1@users.noreply.github.com>

* fix(grain): updated grain trigger to auto-establish trigger (#2666)

Co-authored-by: aadamgough <adam@sim.ai>

* feat(admin): routes to manage deployments (#2667)

* feat(admin): routes to manage deployments

* fix naming fo deployed by

* feat(time-picker): added timepicker emcn component, added to playground, added searchable prop for dropdown, added more timezones for schedule, updated license and notice date (#2668)

* feat(time-picker): added timepicker emcn component, added to playground, added searchable prop for dropdown, added more timezones for schedule, updated license and notice date

* removed unused params, cleaned up redundant utils

* improvement(invite): aligned styling (#2669)

* improvement(invite): aligned with rest of app

* fix(invite): error handling

* fix: addressed comments

---------

Co-authored-by: Emir Karabeg <78010029+emir-karabeg@users.noreply.github.com>
Co-authored-by: Vikhyath Mondreti <vikhyathvikku@gmail.com>
Co-authored-by: waleedlatif1 <waleedlatif1@users.noreply.github.com>
Co-authored-by: Adam Gough <77861281+aadamgough@users.noreply.github.com>
Co-authored-by: aadamgough <adam@sim.ai>
2026-01-03 13:19:18 -08:00
Waleed
eb5d1f3e5b v0.5.48: copy-paste workflow blocks, docs updates, mcp tool fixes 2025-12-31 18:00:04 -08:00
Waleed
54ab82c8dd v0.5.47: deploy workflow as mcp, kb chunks tokenizer, UI improvements, jira service management tools 2025-12-30 23:18:58 -08:00
Waleed
f895bf469b v0.5.46: build improvements, greptile, light mode improvements 2025-12-29 02:17:52 -08:00
Waleed
dd3209af06 v0.5.45: light mode fixes, realtime usage indicator, docker build improvements 2025-12-27 19:57:42 -08:00
Waleed
b6ba3b50a7 v0.5.44: keyboard shortcuts, autolayout, light mode, byok, testing improvements 2025-12-26 21:25:19 -08:00
Waleed
b304233062 v0.5.43: export logs, circleback, grain, vertex, code hygiene, schedule improvements 2025-12-23 19:19:18 -08:00
Vikhyath Mondreti
57e4b49bd6 v0.5.42: fix memory migration 2025-12-23 01:24:54 -08:00
Vikhyath Mondreti
e12dd204ed v0.5.41: memory fixes, copilot improvements, knowledgebase improvements, LLM providers standardization 2025-12-23 00:15:18 -08:00
Vikhyath Mondreti
3d9d9cbc54 v0.5.40: supabase ops to allow non-public schemas, jira uuid 2025-12-21 22:28:05 -08:00
Waleed
0f4ec962ad v0.5.39: notion, workflow variables fixes 2025-12-20 20:44:00 -08:00
Waleed
4827866f9a v0.5.38: snap to grid, copilot ux improvements, billing line items 2025-12-20 17:24:38 -08:00
Waleed
3e697d9ed9 v0.5.37: redaction utils consolidation, logs updates, autoconnect improvements, additional kb tag types 2025-12-19 22:31:55 -08:00
Martin Yankov
4431a1a484 fix(helm): add custom egress rules to realtime network policy (#2481)
The realtime service network policy was missing the custom egress rules section
that allows configuration of additional egress rules via values.yaml. This caused
the realtime pods to be unable to connect to external databases (e.g., PostgreSQL
on port 5432) when using external database configurations.

The app network policy already had this section, but the realtime network policy
was missing it, creating an inconsistency and preventing the realtime service
from accessing external databases configured via networkPolicy.egress values.

This fix adds the same custom egress rules template section to the realtime
network policy, matching the app network policy behavior and allowing users to
configure database connectivity via values.yaml.
2025-12-19 18:59:08 -08:00
Waleed
4d1a9a3f22 v0.5.36: hitl improvements, opengraph, slack fixes, one-click unsubscribe, auth checks, new db indexes 2025-12-19 01:27:49 -08:00
Vikhyath Mondreti
eb07a080fb v0.5.35: helm updates, copilot improvements, 404 for docs, salesforce fixes, subflow resize clamping 2025-12-18 16:23:19 -08:00
11 changed files with 133 additions and 85 deletions

View File

@@ -9,12 +9,12 @@
<p align="center">
<a href="https://sim.ai" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/badge/sim.ai-6F3DFA" alt="Sim.ai"></a>
<a href="https://discord.gg/Hr4UWYEcTT" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/badge/Discord-Join%20Server-5865F2?logo=discord&logoColor=white" alt="Discord"></a>
<a href="https://x.com/simdotai" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/twitter/follow/simdotai?style=social" alt="Twitter"></a>
<a href="https://docs.sim.ai" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/badge/Docs-6F3DFA.svg" alt="Documentation"></a>
<a href="https://x.com/simdotai" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/twitter/follow/simstudioai?style=social" alt="Twitter"></a>
<a href="https://docs.sim.ai" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/badge/Docs-6F3DFA.svg" alt="Documentation"></a> <a href="https://deepwiki.com/simstudioai/sim" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/badge/DeepWiki-1E90FF.svg" alt="DeepWiki"></a>
</p>
<p align="center">
<a href="https://deepwiki.com/simstudioai/sim" target="_blank" rel="noopener noreferrer"><img src="https://deepwiki.com/badge.svg" alt="Ask DeepWiki"></a> <a href="https://cursor.com/link/prompt?text=Help%20me%20set%20up%20Sim%20Studio%20locally.%20Follow%20these%20steps%3A%0A%0A1.%20First%2C%20verify%20Docker%20is%20installed%20and%20running%3A%0A%20%20%20docker%20--version%0A%20%20%20docker%20info%0A%0A2.%20Clone%20the%20repository%3A%0A%20%20%20git%20clone%20https%3A%2F%2Fgithub.com%2Fsimstudioai%2Fsim.git%0A%20%20%20cd%20sim%0A%0A3.%20Start%20the%20services%20with%20Docker%20Compose%3A%0A%20%20%20docker%20compose%20-f%20docker-compose.prod.yml%20up%20-d%0A%0A4.%20Wait%20for%20all%20containers%20to%20be%20healthy%20(this%20may%20take%201-2%20minutes)%3A%0A%20%20%20docker%20compose%20-f%20docker-compose.prod.yml%20ps%0A%0A5.%20Verify%20the%20app%20is%20accessible%20at%20http%3A%2F%2Flocalhost%3A3000%0A%0AIf%20there%20are%20any%20errors%2C%20help%20me%20troubleshoot%20them.%20Common%20issues%3A%0A-%20Port%203000%2C%203002%2C%20or%205432%20already%20in%20use%0A-%20Docker%20not%20running%0A-%20Insufficient%20memory%20(needs%2012GB%2B%20RAM)%0A%0AFor%20local%20AI%20models%20with%20Ollama%2C%20use%20this%20instead%20of%20step%203%3A%0A%20%20%20docker%20compose%20-f%20docker-compose.ollama.yml%20--profile%20setup%20up%20-d"><img src="https://img.shields.io/badge/Set%20Up%20with-Cursor-000000?logo=cursor&logoColor=white" alt="Set Up with Cursor"></a>
<a href="https://cursor.com/link/prompt?text=Help%20me%20set%20up%20Sim%20Studio%20locally.%20Follow%20these%20steps%3A%0A%0A1.%20First%2C%20verify%20Docker%20is%20installed%20and%20running%3A%0A%20%20%20docker%20--version%0A%20%20%20docker%20info%0A%0A2.%20Clone%20the%20repository%3A%0A%20%20%20git%20clone%20https%3A%2F%2Fgithub.com%2Fsimstudioai%2Fsim.git%0A%20%20%20cd%20sim%0A%0A3.%20Start%20the%20services%20with%20Docker%20Compose%3A%0A%20%20%20docker%20compose%20-f%20docker-compose.prod.yml%20up%20-d%0A%0A4.%20Wait%20for%20all%20containers%20to%20be%20healthy%20(this%20may%20take%201-2%20minutes)%3A%0A%20%20%20docker%20compose%20-f%20docker-compose.prod.yml%20ps%0A%0A5.%20Verify%20the%20app%20is%20accessible%20at%20http%3A%2F%2Flocalhost%3A3000%0A%0AIf%20there%20are%20any%20errors%2C%20help%20me%20troubleshoot%20them.%20Common%20issues%3A%0A-%20Port%203000%2C%203002%2C%20or%205432%20already%20in%20use%0A-%20Docker%20not%20running%0A-%20Insufficient%20memory%20(needs%2012GB%2B%20RAM)%0A%0AFor%20local%20AI%20models%20with%20Ollama%2C%20use%20this%20instead%20of%20step%203%3A%0A%20%20%20docker%20compose%20-f%20docker-compose.ollama.yml%20--profile%20setup%20up%20-d"><img src="https://img.shields.io/badge/Set%20Up%20with-Cursor-000000?logo=cursor&logoColor=white" alt="Set Up with Cursor"></a>
</p>
### Build Workflows with Ease

View File

@@ -168,17 +168,12 @@ const NoteMarkdown = memo(function NoteMarkdown({ content }: { content: string }
)
})
export const NoteBlock = memo(function NoteBlock({
id,
data,
selected,
}: NodeProps<NoteBlockNodeData>) {
export const NoteBlock = memo(function NoteBlock({ id, data }: NodeProps<NoteBlockNodeData>) {
const { type, config, name } = data
const { activeWorkflowId, isEnabled, handleClick, hasRing, ringStyles } = useBlockVisual({
blockId: id,
data,
isSelected: selected,
})
const storedValues = useSubBlockStore(
useCallback(

View File

@@ -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, selected }: NodeProps<SubflowNodeData>) => {
export const SubflowNodeComponent = memo(({ data, id }: NodeProps<SubflowNodeData>) => {
const { getNodes } = useReactFlow()
const blockRef = useRef<HTMLDivElement>(null)
const userPermissions = useUserPermissionsContext()
@@ -134,15 +134,13 @@ export const SubflowNodeComponent = memo(({ data, id, selected }: NodeProps<Subf
/**
* Determine the ring styling based on subflow state priority:
* 1. Focused (selected in editor), selected (shift-click/box), or preview selected - blue ring
* 1. Focused (selected in editor) or preview selected - blue ring
* 2. Diff status (version comparison) - green/orange ring
*/
const isSelected = !isPreview && selected
const hasRing =
isFocused || isSelected || isPreviewSelected || diffStatus === 'new' || diffStatus === 'edited'
const hasRing = isFocused || isPreviewSelected || diffStatus === 'new' || diffStatus === 'edited'
const ringStyles = cn(
hasRing && 'ring-[1.75px]',
(isFocused || isSelected || isPreviewSelected) && 'ring-[var(--brand-secondary)]',
(isFocused || isPreviewSelected) && 'ring-[var(--brand-secondary)]',
diffStatus === 'new' && 'ring-[var(--brand-tertiary-2)]',
diffStatus === 'edited' && 'ring-[var(--warning)]'
)
@@ -169,7 +167,7 @@ export const SubflowNodeComponent = memo(({ data, id, selected }: NodeProps<Subf
data-node-id={id}
data-type='subflowNode'
data-nesting-level={nestingLevel}
data-subflow-selected={isFocused || isSelected || isPreviewSelected}
data-subflow-selected={isFocused || isPreviewSelected}
>
{!isPreview && (
<ActionBar blockId={id} blockType={data.kind} disabled={!userPermissions.canEdit} />

View File

@@ -208,6 +208,7 @@ 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)) {
@@ -556,7 +557,6 @@ const SubBlockRow = ({
export const WorkflowBlock = memo(function WorkflowBlock({
id,
data,
selected,
}: NodeProps<WorkflowBlockProps>) {
const { type, config, name, isPending } = data
@@ -574,7 +574,7 @@ export const WorkflowBlock = memo(function WorkflowBlock({
hasRing,
ringStyles,
runPathStatus,
} = useBlockVisual({ blockId: id, data, isPending, isSelected: selected })
} = useBlockVisual({ blockId: id, data, isPending })
const currentBlock = currentWorkflow.getBlockById(id)

View File

@@ -17,8 +17,6 @@ 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
}
/**
@@ -30,12 +28,7 @@ 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,
isSelected = false,
}: UseBlockVisualProps) {
export function useBlockVisual({ blockId, data, isPending = false }: UseBlockVisualProps) {
const isPreview = data.isPreview ?? false
const isPreviewSelected = data.isPreviewSelected ?? false
@@ -49,6 +42,7 @@ export function useBlockVisual({
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'
@@ -74,7 +68,6 @@ export function useBlockVisual({
diffStatus: isPreview ? undefined : diffStatus,
runPathStatus,
isPreviewSelection: isPreview && isPreviewSelected,
isSelected: isPreview ? false : isSelected,
}),
[
isExecuting,
@@ -85,7 +78,6 @@ export function useBlockVisual({
runPathStatus,
isPreview,
isPreviewSelected,
isSelected,
]
)

View File

@@ -14,8 +14,6 @@ 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
}
/**
@@ -34,13 +32,11 @@ export function getBlockRingStyles(options: BlockRingOptions): {
diffStatus,
runPathStatus,
isPreviewSelection,
isSelected,
} = options
const hasRing =
isExecuting ||
isEditorOpen ||
isSelected ||
isPending ||
diffStatus === 'new' ||
diffStatus === 'edited' ||
@@ -50,37 +46,25 @@ 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, selected, or preview selection: static blue ring
// Editor open or preview selection: static blue ring
!isExecuting &&
(isEditorOpen || isSelected || isPreviewSelection) &&
(isEditorOpen || isPreviewSelection) &&
'ring-[1.75px] ring-[var(--brand-secondary)]',
// Non-active states use standard ring utilities
!isExecuting &&
!isEditorOpen &&
!isSelected &&
!isPreviewSelection &&
hasRing &&
'ring-[1.75px]',
!isExecuting && !isEditorOpen && !isPreviewSelection && hasRing && 'ring-[1.75px]',
// Pending state: warning ring
!isExecuting && !isEditorOpen && !isSelected && isPending && 'ring-[var(--warning)]',
!isExecuting && !isEditorOpen && isPending && 'ring-[var(--warning)]',
// Deleted state (highest priority after active/pending)
!isExecuting &&
!isEditorOpen &&
!isSelected &&
!isPending &&
isDeletedBlock &&
'ring-[var(--text-error)]',
!isExecuting && !isEditorOpen && !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' &&
@@ -88,7 +72,6 @@ export function getBlockRingStyles(options: BlockRingOptions): {
// Run path states (lowest priority - only show if no other states active)
!isExecuting &&
!isEditorOpen &&
!isSelected &&
!isPending &&
!isDeletedBlock &&
!diffStatus &&
@@ -96,7 +79,6 @@ export function getBlockRingStyles(options: BlockRingOptions): {
'ring-[var(--border-success)]',
!isExecuting &&
!isEditorOpen &&
!isSelected &&
!isPending &&
!isDeletedBlock &&
!diffStatus &&

View File

@@ -700,23 +700,7 @@ const WorkflowContent = React.memo(() => {
triggerMode,
})
const subBlockValues: Record<string, Record<string, unknown>> = {}
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
)
collaborativeBatchAddBlocks([block], autoConnectEdge ? [autoConnectEdge] : [], {}, {}, {})
usePanelEditorStore.getState().setCurrentBlockId(id)
},
[collaborativeBatchAddBlocks, setSelectedEdges]

View File

@@ -406,13 +406,21 @@ 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) {
logger.debug('Received cursor-update for unknown user', { socketId: data.socketId })
return prev
if (existingIndex !== -1) {
return prev.map((user) =>
user.socketId === data.socketId ? { ...user, cursor: data.cursor } : user
)
}
return prev.map((user) =>
user.socketId === data.socketId ? { ...user, cursor: data.cursor } : user
)
return [
...prev,
{
socketId: data.socketId,
userId: data.userId,
userName: data.userName,
avatarUrl: data.avatarUrl,
cursor: data.cursor,
},
]
})
eventHandlers.current.cursorUpdate?.(data)
})
@@ -420,15 +428,21 @@ 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) {
logger.debug('Received selection-update for unknown user', {
socketId: data.socketId,
})
return prev
if (existingIndex !== -1) {
return prev.map((user) =>
user.socketId === data.socketId ? { ...user, selection: data.selection } : user
)
}
return prev.map((user) =>
user.socketId === data.socketId ? { ...user, selection: data.selection } : user
)
return [
...prev,
{
socketId: data.socketId,
userId: data.userId,
userName: data.userName,
avatarUrl: data.avatarUrl,
selection: data.selection,
},
]
})
eventHandlers.current.selectionUpdate?.(data)
})

View File

@@ -2499,9 +2499,7 @@ export const editWorkflowServerTool: BaseServerTool<EditWorkflowParams, any> = {
async execute(params: EditWorkflowParams, context?: { userId: string }): Promise<any> {
const logger = createLogger('EditWorkflowServerTool')
const { operations, workflowId, currentUserWorkflow } = params
if (!Array.isArray(operations) || operations.length === 0) {
throw new Error('operations are required and must be an array')
}
if (!operations || operations.length === 0) throw new Error('operations are required')
if (!workflowId) throw new Error('workflowId is required')
logger.info('Executing edit_workflow', {

View File

@@ -1,6 +1,7 @@
import crypto from 'crypto'
import {
db,
webhook,
workflow,
workflowBlocks,
workflowDeploymentVersion,
@@ -21,6 +22,7 @@ import { generateLoopBlocks, generateParallelBlocks } from '@/stores/workflows/w
const logger = createLogger('WorkflowDBHelpers')
export type WorkflowDeploymentVersion = InferSelectModel<typeof workflowDeploymentVersion>
type WebhookRecord = InferSelectModel<typeof webhook>
type SubflowInsert = InferInsertModel<typeof workflowSubflows>
export interface WorkflowDeploymentVersionResponse {
@@ -335,6 +337,18 @@ 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)),
@@ -405,6 +419,42 @@ 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 }

View File

@@ -1,6 +1,7 @@
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'
@@ -1174,6 +1175,14 @@ 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
@@ -1239,6 +1248,32 @@ async function handleWorkflowOperationTx(
await tx.insert(workflowSubflows).values(parallelValues)
}
// Re-insert preserved webhooks if any exist and their blocks still exist
type WebhookRecord = InferSelectModel<typeof webhook>
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
}