diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown.tsx index 37d2b57c2..b58752dee 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown.tsx @@ -1312,15 +1312,16 @@ export const TagDropdown: React.FC = ({ if (currentLoop && isLoopBlock) { containingLoopBlockId = blockId const loopType = currentLoop.loopType || 'for' - const contextualTags: string[] = ['index'] - if (loopType === 'forEach') { - contextualTags.push('currentItem') - contextualTags.push('items') - } const loopBlock = blocks[blockId] if (loopBlock) { const loopBlockName = loopBlock.name || loopBlock.type + const normalizedLoopName = normalizeName(loopBlockName) + const contextualTags: string[] = [`${normalizedLoopName}.index`] + if (loopType === 'forEach') { + contextualTags.push(`${normalizedLoopName}.currentItem`) + contextualTags.push(`${normalizedLoopName}.items`) + } loopBlockGroup = { blockName: loopBlockName, @@ -1328,21 +1329,23 @@ export const TagDropdown: React.FC = ({ blockType: 'loop', tags: contextualTags, distance: 0, + isContextual: true, } } } else if (containingLoop) { const [loopId, loop] = containingLoop containingLoopBlockId = loopId const loopType = loop.loopType || 'for' - const contextualTags: string[] = ['index'] - if (loopType === 'forEach') { - contextualTags.push('currentItem') - contextualTags.push('items') - } const containingLoopBlock = blocks[loopId] if (containingLoopBlock) { const loopBlockName = containingLoopBlock.name || containingLoopBlock.type + const normalizedLoopName = normalizeName(loopBlockName) + const contextualTags: string[] = [`${normalizedLoopName}.index`] + if (loopType === 'forEach') { + contextualTags.push(`${normalizedLoopName}.currentItem`) + contextualTags.push(`${normalizedLoopName}.items`) + } loopBlockGroup = { blockName: loopBlockName, @@ -1350,6 +1353,7 @@ export const TagDropdown: React.FC = ({ blockType: 'loop', tags: contextualTags, distance: 0, + isContextual: true, } } } @@ -1363,15 +1367,16 @@ export const TagDropdown: React.FC = ({ const [parallelId, parallel] = containingParallel containingParallelBlockId = parallelId const parallelType = parallel.parallelType || 'count' - const contextualTags: string[] = ['index'] - if (parallelType === 'collection') { - contextualTags.push('currentItem') - contextualTags.push('items') - } const containingParallelBlock = blocks[parallelId] if (containingParallelBlock) { const parallelBlockName = containingParallelBlock.name || containingParallelBlock.type + const normalizedParallelName = normalizeName(parallelBlockName) + const contextualTags: string[] = [`${normalizedParallelName}.index`] + if (parallelType === 'collection') { + contextualTags.push(`${normalizedParallelName}.currentItem`) + contextualTags.push(`${normalizedParallelName}.items`) + } parallelBlockGroup = { blockName: parallelBlockName, @@ -1379,6 +1384,7 @@ export const TagDropdown: React.FC = ({ blockType: 'parallel', tags: contextualTags, distance: 0, + isContextual: true, } } } @@ -1645,38 +1651,29 @@ export const TagDropdown: React.FC = ({ const nestedBlockTagGroups: NestedBlockTagGroup[] = useMemo(() => { return filteredBlockTagGroups.map((group: BlockTagGroup) => { const normalizedBlockName = normalizeName(group.blockName) - - // Handle loop/parallel contextual tags (index, currentItem, items) const directTags: NestedTag[] = [] const tagsForTree: string[] = [] group.tags.forEach((tag: string) => { const tagParts = tag.split('.') - // Loop/parallel contextual tags without block prefix - if ( - (group.blockType === 'loop' || group.blockType === 'parallel') && - tagParts.length === 1 - ) { + if (tagParts.length === 1) { directTags.push({ key: tag, display: tag, fullTag: tag, }) } else if (tagParts.length === 2) { - // Direct property like blockname.property directTags.push({ key: tagParts[1], display: tagParts[1], fullTag: tag, }) } else { - // Nested property - add to tree builder tagsForTree.push(tag) } }) - // Build recursive tree from nested tags const nestedTags = [...directTags, ...buildNestedTagTree(tagsForTree, normalizedBlockName)] return { @@ -1800,13 +1797,19 @@ export const TagDropdown: React.FC = ({ processedTag = tag } } else if ( - blockGroup && + blockGroup?.isContextual && (blockGroup.blockType === 'loop' || blockGroup.blockType === 'parallel') ) { - if (!tag.includes('.') && ['index', 'currentItem', 'items'].includes(tag)) { - processedTag = `${blockGroup.blockType}.${tag}` + const tagParts = tag.split('.') + if (tagParts.length === 1) { + processedTag = blockGroup.blockType } else { - processedTag = tag + const lastPart = tagParts[tagParts.length - 1] + if (['index', 'currentItem', 'items'].includes(lastPart)) { + processedTag = `${blockGroup.blockType}.${lastPart}` + } else { + processedTag = tag + } } } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/types.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/types.ts index d17fc2874..60e8ccedc 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/types.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/types.ts @@ -7,6 +7,8 @@ export interface BlockTagGroup { blockType: string tags: string[] distance: number + /** True if this is a contextual group (loop/parallel iteration context available inside the subflow) */ + isContextual?: boolean } /** diff --git a/apps/sim/executor/constants.ts b/apps/sim/executor/constants.ts index d473472c8..387f560c4 100644 --- a/apps/sim/executor/constants.ts +++ b/apps/sim/executor/constants.ts @@ -120,6 +120,12 @@ export const SPECIAL_REFERENCE_PREFIXES = [ REFERENCE.PREFIX.VARIABLE, ] as const +export const RESERVED_BLOCK_NAMES = [ + REFERENCE.PREFIX.LOOP, + REFERENCE.PREFIX.PARALLEL, + REFERENCE.PREFIX.VARIABLE, +] as const + export const LOOP_REFERENCE = { ITERATION: 'iteration', INDEX: 'index', diff --git a/apps/sim/executor/variables/resolvers/loop.test.ts b/apps/sim/executor/variables/resolvers/loop.test.ts index 5faf88936..faabb48ca 100644 --- a/apps/sim/executor/variables/resolvers/loop.test.ts +++ b/apps/sim/executor/variables/resolvers/loop.test.ts @@ -1,6 +1,7 @@ import { loggerMock } from '@sim/testing' import { describe, expect, it, vi } from 'vitest' import type { LoopScope } from '@/executor/execution/state' +import { InvalidFieldError } from '@/executor/utils/block-reference' import { LoopResolver } from './loop' import type { ResolutionContext } from './reference' @@ -62,7 +63,12 @@ function createTestContext( describe('LoopResolver', () => { describe('canResolve', () => { - it.concurrent('should return true for loop references', () => { + it.concurrent('should return true for bare loop reference', () => { + const resolver = new LoopResolver(createTestWorkflow()) + expect(resolver.canResolve('')).toBe(true) + }) + + it.concurrent('should return true for known loop properties', () => { const resolver = new LoopResolver(createTestWorkflow()) expect(resolver.canResolve('')).toBe(true) expect(resolver.canResolve('')).toBe(true) @@ -78,6 +84,13 @@ describe('LoopResolver', () => { expect(resolver.canResolve('')).toBe(true) }) + it.concurrent('should return true for unknown loop properties (validates in resolve)', () => { + const resolver = new LoopResolver(createTestWorkflow()) + expect(resolver.canResolve('')).toBe(true) + expect(resolver.canResolve('')).toBe(true) + expect(resolver.canResolve('')).toBe(true) + }) + it.concurrent('should return false for non-loop references', () => { const resolver = new LoopResolver(createTestWorkflow()) expect(resolver.canResolve('')).toBe(false) @@ -181,20 +194,34 @@ describe('LoopResolver', () => { }) describe('edge cases', () => { - it.concurrent('should return undefined for invalid loop reference (missing property)', () => { + it.concurrent('should return context object for bare loop reference', () => { const resolver = new LoopResolver(createTestWorkflow()) - const loopScope = createLoopScope({ iteration: 0 }) + const loopScope = createLoopScope({ iteration: 2, item: 'test', items: ['a', 'b', 'c'] }) const ctx = createTestContext('block-1', loopScope) - expect(resolver.resolve('', ctx)).toBeUndefined() + expect(resolver.resolve('', ctx)).toEqual({ + index: 2, + currentItem: 'test', + items: ['a', 'b', 'c'], + }) }) - it.concurrent('should return undefined for unknown loop property', () => { + it.concurrent('should return minimal context object for for-loop (no items)', () => { + const resolver = new LoopResolver(createTestWorkflow()) + const loopScope = createLoopScope({ iteration: 5 }) + const ctx = createTestContext('block-1', loopScope) + + expect(resolver.resolve('', ctx)).toEqual({ + index: 5, + }) + }) + + it.concurrent('should throw InvalidFieldError for unknown loop property', () => { const resolver = new LoopResolver(createTestWorkflow()) const loopScope = createLoopScope({ iteration: 0 }) const ctx = createTestContext('block-1', loopScope) - expect(resolver.resolve('', ctx)).toBeUndefined() + expect(() => resolver.resolve('', ctx)).toThrow(InvalidFieldError) }) it.concurrent('should handle iteration index 0 correctly', () => { diff --git a/apps/sim/executor/variables/resolvers/loop.ts b/apps/sim/executor/variables/resolvers/loop.ts index 69d0f5431..3abaec58c 100644 --- a/apps/sim/executor/variables/resolvers/loop.ts +++ b/apps/sim/executor/variables/resolvers/loop.ts @@ -1,5 +1,6 @@ import { createLogger } from '@sim/logger' import { isReference, parseReferencePath, REFERENCE } from '@/executor/constants' +import { InvalidFieldError } from '@/executor/utils/block-reference' import { extractBaseBlockId } from '@/executor/utils/subflow-utils' import { navigatePath, @@ -13,6 +14,8 @@ const logger = createLogger('LoopResolver') export class LoopResolver implements Resolver { constructor(private workflow: SerializedWorkflow) {} + private static KNOWN_PROPERTIES = ['iteration', 'index', 'item', 'currentItem', 'items'] + canResolve(reference: string): boolean { if (!isReference(reference)) { return false @@ -27,16 +30,15 @@ export class LoopResolver implements Resolver { resolve(reference: string, context: ResolutionContext): any { const parts = parseReferencePath(reference) - if (parts.length < 2) { - logger.warn('Invalid loop reference - missing property', { reference }) + if (parts.length === 0) { + logger.warn('Invalid loop reference', { reference }) return undefined } - const [_, property, ...pathParts] = parts + const loopId = this.findLoopForBlock(context.currentNodeId) let loopScope = context.loopScope if (!loopScope) { - const loopId = this.findLoopForBlock(context.currentNodeId) if (!loopId) { return undefined } @@ -48,6 +50,27 @@ export class LoopResolver implements Resolver { return undefined } + const isForEach = loopId ? this.isForEachLoop(loopId) : loopScope.items !== undefined + + if (parts.length === 1) { + const result: Record = { + index: loopScope.iteration, + } + if (loopScope.item !== undefined) { + result.currentItem = loopScope.item + } + if (loopScope.items !== undefined) { + result.items = loopScope.items + } + return result + } + + const [_, property, ...pathParts] = parts + if (!LoopResolver.KNOWN_PROPERTIES.includes(property)) { + const availableFields = isForEach ? ['index', 'currentItem', 'items'] : ['index'] + throw new InvalidFieldError('loop', property, availableFields) + } + let value: any switch (property) { case 'iteration': @@ -61,12 +84,8 @@ export class LoopResolver implements Resolver { case 'items': value = loopScope.items break - default: - logger.warn('Unknown loop property', { property }) - return undefined } - // If there are additional path parts, navigate deeper if (pathParts.length > 0) { return navigatePath(value, pathParts) } @@ -85,4 +104,9 @@ export class LoopResolver implements Resolver { return undefined } + + private isForEachLoop(loopId: string): boolean { + const loopConfig = this.workflow.loops?.[loopId] + return loopConfig?.loopType === 'forEach' + } } diff --git a/apps/sim/executor/variables/resolvers/parallel.test.ts b/apps/sim/executor/variables/resolvers/parallel.test.ts index a2d18ed0d..9466fcad2 100644 --- a/apps/sim/executor/variables/resolvers/parallel.test.ts +++ b/apps/sim/executor/variables/resolvers/parallel.test.ts @@ -1,5 +1,6 @@ import { loggerMock } from '@sim/testing' import { describe, expect, it, vi } from 'vitest' +import { InvalidFieldError } from '@/executor/utils/block-reference' import { ParallelResolver } from './parallel' import type { ResolutionContext } from './reference' @@ -81,7 +82,12 @@ function createTestContext( describe('ParallelResolver', () => { describe('canResolve', () => { - it.concurrent('should return true for parallel references', () => { + it.concurrent('should return true for bare parallel reference', () => { + const resolver = new ParallelResolver(createTestWorkflow()) + expect(resolver.canResolve('')).toBe(true) + }) + + it.concurrent('should return true for known parallel properties', () => { const resolver = new ParallelResolver(createTestWorkflow()) expect(resolver.canResolve('')).toBe(true) expect(resolver.canResolve('')).toBe(true) @@ -94,6 +100,16 @@ describe('ParallelResolver', () => { expect(resolver.canResolve('')).toBe(true) }) + it.concurrent( + 'should return true for unknown parallel properties (validates in resolve)', + () => { + const resolver = new ParallelResolver(createTestWorkflow()) + expect(resolver.canResolve('')).toBe(true) + expect(resolver.canResolve('')).toBe(true) + expect(resolver.canResolve('')).toBe(true) + } + ) + it.concurrent('should return false for non-parallel references', () => { const resolver = new ParallelResolver(createTestWorkflow()) expect(resolver.canResolve('')).toBe(false) @@ -254,24 +270,40 @@ describe('ParallelResolver', () => { }) describe('edge cases', () => { - it.concurrent( - 'should return undefined for invalid parallel reference (missing property)', - () => { - const resolver = new ParallelResolver(createTestWorkflow()) - const ctx = createTestContext('block-1₍0₎') + it.concurrent('should return context object for bare parallel reference', () => { + const workflow = createTestWorkflow({ + 'parallel-1': { nodes: ['block-1'], distribution: ['a', 'b', 'c'] }, + }) + const resolver = new ParallelResolver(workflow) + const ctx = createTestContext('block-1₍1₎') - expect(resolver.resolve('', ctx)).toBeUndefined() - } - ) + expect(resolver.resolve('', ctx)).toEqual({ + index: 1, + currentItem: 'b', + items: ['a', 'b', 'c'], + }) + }) - it.concurrent('should return undefined for unknown parallel property', () => { + it.concurrent('should return minimal context object when no distribution', () => { + const workflow = createTestWorkflow({ + 'parallel-1': { nodes: ['block-1'] }, + }) + const resolver = new ParallelResolver(workflow) + const ctx = createTestContext('block-1₍0₎') + + const result = resolver.resolve('', ctx) + expect(result).toHaveProperty('index', 0) + expect(result).toHaveProperty('items') + }) + + it.concurrent('should throw InvalidFieldError for unknown parallel property', () => { const workflow = createTestWorkflow({ 'parallel-1': { nodes: ['block-1'], distribution: ['a'] }, }) const resolver = new ParallelResolver(workflow) const ctx = createTestContext('block-1₍0₎') - expect(resolver.resolve('', ctx)).toBeUndefined() + expect(() => resolver.resolve('', ctx)).toThrow(InvalidFieldError) }) it.concurrent('should return undefined when block is not in any parallel', () => { diff --git a/apps/sim/executor/variables/resolvers/parallel.ts b/apps/sim/executor/variables/resolvers/parallel.ts index c736e8536..4481d0fe4 100644 --- a/apps/sim/executor/variables/resolvers/parallel.ts +++ b/apps/sim/executor/variables/resolvers/parallel.ts @@ -1,5 +1,6 @@ import { createLogger } from '@sim/logger' import { isReference, parseReferencePath, REFERENCE } from '@/executor/constants' +import { InvalidFieldError } from '@/executor/utils/block-reference' import { extractBaseBlockId, extractBranchIndex } from '@/executor/utils/subflow-utils' import { navigatePath, @@ -13,6 +14,8 @@ const logger = createLogger('ParallelResolver') export class ParallelResolver implements Resolver { constructor(private workflow: SerializedWorkflow) {} + private static KNOWN_PROPERTIES = ['index', 'currentItem', 'items'] + canResolve(reference: string): boolean { if (!isReference(reference)) { return false @@ -27,12 +30,11 @@ export class ParallelResolver implements Resolver { resolve(reference: string, context: ResolutionContext): any { const parts = parseReferencePath(reference) - if (parts.length < 2) { - logger.warn('Invalid parallel reference - missing property', { reference }) + if (parts.length === 0) { + logger.warn('Invalid parallel reference', { reference }) return undefined } - const [_, property, ...pathParts] = parts const parallelId = this.findParallelForBlock(context.currentNodeId) if (!parallelId) { return undefined @@ -49,11 +51,33 @@ export class ParallelResolver implements Resolver { return undefined } - // First try to get items from the parallel scope (resolved at runtime) - // This is the same pattern as LoopResolver reading from loopScope.items const parallelScope = context.executionContext.parallelExecutions?.get(parallelId) const distributionItems = parallelScope?.items ?? this.getDistributionItems(parallelConfig) + if (parts.length === 1) { + const result: Record = { + index: branchIndex, + } + if (distributionItems !== undefined) { + result.items = distributionItems + if (Array.isArray(distributionItems)) { + result.currentItem = distributionItems[branchIndex] + } else if (typeof distributionItems === 'object' && distributionItems !== null) { + const keys = Object.keys(distributionItems) + const key = keys[branchIndex] + result.currentItem = key !== undefined ? distributionItems[key] : undefined + } + } + return result + } + + const [_, property, ...pathParts] = parts + if (!ParallelResolver.KNOWN_PROPERTIES.includes(property)) { + const isCollection = parallelConfig.parallelType === 'collection' + const availableFields = isCollection ? ['index', 'currentItem', 'items'] : ['index'] + throw new InvalidFieldError('parallel', property, availableFields) + } + let value: any switch (property) { case 'index': @@ -73,12 +97,8 @@ export class ParallelResolver implements Resolver { case 'items': value = distributionItems break - default: - logger.warn('Unknown parallel property', { property }) - return undefined } - // If there are additional path parts, navigate deeper if (pathParts.length > 0) { return navigatePath(value, pathParts) } diff --git a/apps/sim/hooks/use-collaborative-workflow.ts b/apps/sim/hooks/use-collaborative-workflow.ts index 1b39b7676..43f512998 100644 --- a/apps/sim/hooks/use-collaborative-workflow.ts +++ b/apps/sim/hooks/use-collaborative-workflow.ts @@ -5,7 +5,7 @@ import { useShallow } from 'zustand/react/shallow' import { useSession } from '@/lib/auth/auth-client' import { useSocket } from '@/app/workspace/providers/socket-provider' import { getBlock } from '@/blocks' -import { normalizeName } from '@/executor/constants' +import { normalizeName, RESERVED_BLOCK_NAMES } from '@/executor/constants' import { useUndoRedo } from '@/hooks/use-undo-redo' import { BLOCK_OPERATIONS, @@ -740,6 +740,16 @@ export function useCollaborativeWorkflow() { return { success: false, error: 'Block name cannot be empty' } } + if ((RESERVED_BLOCK_NAMES as readonly string[]).includes(normalizedNewName)) { + logger.error(`Cannot rename block to reserved name: "${trimmedName}"`) + useNotificationStore.getState().addNotification({ + level: 'error', + message: `"${trimmedName}" is a reserved name and cannot be used`, + workflowId: activeWorkflowId || undefined, + }) + return { success: false, error: `"${trimmedName}" is a reserved name` } + } + const currentBlocks = useWorkflowStore.getState().blocks const conflictingBlock = Object.entries(currentBlocks).find( ([blockId, block]) => blockId !== id && normalizeName(block.name) === normalizedNewName 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 48e5f74d5..34de7723f 100644 --- a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts +++ b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts @@ -14,7 +14,7 @@ import { validateWorkflowState } from '@/lib/workflows/sanitization/validation' import { TriggerUtils } from '@/lib/workflows/triggers/triggers' import { getAllBlocks, getBlock } from '@/blocks/registry' import type { SubBlockConfig } from '@/blocks/types' -import { EDGE, normalizeName } from '@/executor/constants' +import { EDGE, normalizeName, RESERVED_BLOCK_NAMES } from '@/executor/constants' import { getUserPermissionConfig } from '@/executor/utils/permission-check' import { generateLoopBlocks, generateParallelBlocks } from '@/stores/workflows/workflow/utils' import { TRIGGER_RUNTIME_SUBBLOCK_IDS } from '@/triggers/constants' @@ -63,6 +63,7 @@ type SkippedItemType = | 'invalid_subflow_parent' | 'nested_subflow_not_allowed' | 'duplicate_block_name' + | 'reserved_block_name' | 'duplicate_trigger' | 'duplicate_single_instance_block' @@ -1683,7 +1684,8 @@ function applyOperationsToWorkflowState( } } if (params?.name !== undefined) { - if (!normalizeName(params.name)) { + const normalizedName = normalizeName(params.name) + if (!normalizedName) { logSkippedItem(skippedItems, { type: 'missing_required_params', operationType: 'edit', @@ -1691,6 +1693,14 @@ function applyOperationsToWorkflowState( reason: `Cannot rename to empty name`, details: { requestedName: params.name }, }) + } else if ((RESERVED_BLOCK_NAMES as readonly string[]).includes(normalizedName)) { + logSkippedItem(skippedItems, { + type: 'reserved_block_name', + operationType: 'edit', + blockId: block_id, + reason: `Cannot rename to "${params.name}" - this is a reserved name`, + details: { requestedName: params.name }, + }) } else { const conflictingBlock = findBlockWithDuplicateNormalizedName( modifiedState.blocks, @@ -1911,7 +1921,8 @@ function applyOperationsToWorkflowState( } case 'add': { - if (!params?.type || !params?.name || !normalizeName(params.name)) { + const addNormalizedName = params?.name ? normalizeName(params.name) : '' + if (!params?.type || !params?.name || !addNormalizedName) { logSkippedItem(skippedItems, { type: 'missing_required_params', operationType: 'add', @@ -1922,6 +1933,17 @@ function applyOperationsToWorkflowState( break } + if ((RESERVED_BLOCK_NAMES as readonly string[]).includes(addNormalizedName)) { + logSkippedItem(skippedItems, { + type: 'reserved_block_name', + operationType: 'add', + blockId: block_id, + reason: `Block name "${params.name}" is a reserved name and cannot be used`, + details: { requestedName: params.name }, + }) + break + } + const conflictingBlock = findBlockWithDuplicateNormalizedName( modifiedState.blocks, params.name, diff --git a/apps/sim/stores/workflows/workflow/store.ts b/apps/sim/stores/workflows/workflow/store.ts index 27f9716f0..9c40fffc4 100644 --- a/apps/sim/stores/workflows/workflow/store.ts +++ b/apps/sim/stores/workflows/workflow/store.ts @@ -7,7 +7,7 @@ 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 } from '@/executor/constants' +import { 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' @@ -726,6 +726,11 @@ export const useWorkflowStore = create()( return { success: false, changedSubblocks: [] } } + if ((RESERVED_BLOCK_NAMES as readonly string[]).includes(normalizedNewName)) { + logger.error(`Cannot rename block to reserved name: "${name}"`) + return { success: false, changedSubblocks: [] } + } + const newState = { blocks: { ...get().blocks,