Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
5 changes: 4 additions & 1 deletion apps/sim/lib/workflows/operations/import-export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -583,7 +583,10 @@ export function parseWorkflowJson(
loops: workflowData.loops || {},
parallels: workflowData.parallels || {},
metadata: workflowData.metadata,
variables: Array.isArray(workflowData.variables) ? workflowData.variables : undefined,
variables:
workflowData.variables && typeof workflowData.variables === 'object'
? workflowData.variables
: undefined,
}

if (regenerateIdsFlag) {
Expand Down
59 changes: 58 additions & 1 deletion apps/sim/lib/workflows/persistence/duplicate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,44 @@ interface DuplicateWorkflowResult {
subflowsCount: number
}

/**
* Remaps old variable IDs to new variable IDs inside block subBlocks.
* Specifically targets `variables-input` subblocks whose value is an array
* of variable assignments containing a `variableId` field.
*/
function remapVariableIdsInSubBlocks(
subBlocks: Record<string, any>,
varIdMap: Map<string, string>
): Record<string, any> {
const updated: Record<string, any> = {}

for (const [key, subBlock] of Object.entries(subBlocks)) {
if (
subBlock &&
typeof subBlock === 'object' &&
subBlock.type === 'variables-input' &&
Array.isArray(subBlock.value)
) {
updated[key] = {
...subBlock,
value: subBlock.value.map((assignment: any) => {
if (assignment && typeof assignment === 'object' && assignment.variableId) {
const newVarId = varIdMap.get(assignment.variableId)
if (newVarId) {
return { ...assignment, variableId: newVarId }
}
}
return assignment
}),
}
} else {
updated[key] = subBlock
}
}

return updated
}

/**
* Duplicate a workflow with all its blocks, edges, and subflows
* This is a shared helper used by both the workflow duplicate API and folder duplicate API
Expand Down Expand Up @@ -104,6 +142,9 @@ export async function duplicateWorkflow(
.where(and(eq(workflow.workspaceId, targetWorkspaceId), folderCondition))
const sortOrder = (minResult?.minOrder ?? 1) - 1

// Mapping from old variable IDs to new variable IDs (populated during variable duplication)
const varIdMapping = new Map<string, string>()

// Create the new workflow first (required for foreign key constraints)
await tx.insert(workflow).values({
id: newWorkflowId,
Expand All @@ -123,8 +164,9 @@ export async function duplicateWorkflow(
variables: (() => {
const sourceVars = (source.variables as Record<string, Variable>) || {}
const remapped: Record<string, Variable> = {}
for (const [, variable] of Object.entries(sourceVars) as [string, Variable][]) {
for (const [oldVarId, variable] of Object.entries(sourceVars) as [string, Variable][]) {
const newVarId = crypto.randomUUID()
varIdMapping.set(oldVarId, newVarId)
remapped[newVarId] = {
...variable,
id: newVarId,
Expand Down Expand Up @@ -181,13 +223,28 @@ export async function duplicateWorkflow(
}
}

// Update variable references in subBlocks (e.g. variables-input assignments)
let updatedSubBlocks = block.subBlocks
if (
varIdMapping.size > 0 &&
block.subBlocks &&
typeof block.subBlocks === 'object' &&
!Array.isArray(block.subBlocks)
) {
updatedSubBlocks = remapVariableIdsInSubBlocks(
block.subBlocks as Record<string, any>,
varIdMapping
)
}

return {
...block,
id: newBlockId,
workflowId: newWorkflowId,
parentId: newParentId,
extent: newExtent,
data: updatedData,
subBlocks: updatedSubBlocks,
locked: false, // Duplicated blocks should always be unlocked
createdAt: now,
updatedAt: now,
Expand Down
15 changes: 9 additions & 6 deletions apps/sim/lib/workflows/sanitization/json-sanitizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,15 @@ export interface ExportWorkflowState {
sortOrder?: number
exportedAt?: string
}
variables?: Array<{
id: string
name: string
type: 'string' | 'number' | 'boolean' | 'object' | 'array' | 'plain'
value: unknown
}>
variables?: Record<
string,
{
id: string
name: string
type: 'string' | 'number' | 'boolean' | 'object' | 'array' | 'plain'
value: unknown
}
>
}
}

Expand Down