Skip to content

Commit 9a16e7c

Browse files
improvement(response): only allow singleton (#2764)
* improvement(response): only allow singleton * respect singleton triggers and blocks in copilot * don't show dup button for response * fix error message
1 parent 283a521 commit 9a16e7c

File tree

8 files changed

+94
-8
lines changed

8 files changed

+94
-8
lines changed

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/action-bar/action-bar.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,8 @@ export const ActionBar = memo(
8787

8888
const userPermissions = useUserPermissionsContext()
8989

90-
// Check for start_trigger (unified start block) - prevent duplication but allow deletion
9190
const isStartBlock = blockType === 'starter' || blockType === 'start_trigger'
91+
const isResponseBlock = blockType === 'response'
9292
const isNoteBlock = blockType === 'note'
9393

9494
/**
@@ -140,7 +140,7 @@ export const ActionBar = memo(
140140
</Tooltip.Root>
141141
)}
142142

143-
{!isStartBlock && (
143+
{!isStartBlock && !isResponseBlock && (
144144
<Tooltip.Root>
145145
<Tooltip.Trigger asChild>
146146
<Button

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/workflow-canvas-helpers.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ interface TriggerValidationResult {
2323
}
2424

2525
/**
26-
* Validates that pasting/duplicating trigger blocks won't violate constraints.
26+
* Validates that pasting/duplicating blocks won't violate constraints.
27+
* Checks both trigger constraints and single-instance block constraints.
2728
* Returns validation result with error message if invalid.
2829
*/
2930
export function validateTriggerPaste(
@@ -43,6 +44,12 @@ export function validateTriggerPaste(
4344
return { isValid: false, message }
4445
}
4546
}
47+
48+
const singleInstanceIssue = TriggerUtils.getSingleInstanceBlockIssue(existingBlocks, block.type)
49+
if (singleInstanceIssue) {
50+
const message = `A workflow can only have one ${singleInstanceIssue.blockName} block. ${action === 'paste' ? 'Please remove the existing one before pasting.' : 'Cannot duplicate.'}`
51+
return { isValid: false, message }
52+
}
4653
}
4754
return { isValid: true }
4855
}

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1129,24 +1129,36 @@ const WorkflowContent = React.memo(() => {
11291129
)
11301130

11311131
/**
1132-
* Checks if adding a trigger block would violate constraints and shows notification if so.
1132+
* Checks if adding a block would violate constraints (triggers or single-instance blocks)
1133+
* and shows notification if so.
11331134
* @returns true if validation failed (caller should return early), false if ok to proceed
11341135
*/
11351136
const checkTriggerConstraints = useCallback(
11361137
(blockType: string): boolean => {
1137-
const issue = TriggerUtils.getTriggerAdditionIssue(blocks, blockType)
1138-
if (issue) {
1138+
const triggerIssue = TriggerUtils.getTriggerAdditionIssue(blocks, blockType)
1139+
if (triggerIssue) {
11391140
const message =
1140-
issue.issue === 'legacy'
1141+
triggerIssue.issue === 'legacy'
11411142
? 'Cannot add new trigger blocks when a legacy Start block exists. Available in newer workflows.'
1142-
: `A workflow can only have one ${issue.triggerName} trigger block. Please remove the existing one before adding a new one.`
1143+
: `A workflow can only have one ${triggerIssue.triggerName} trigger block. Please remove the existing one before adding a new one.`
11431144
addNotification({
11441145
level: 'error',
11451146
message,
11461147
workflowId: activeWorkflowId || undefined,
11471148
})
11481149
return true
11491150
}
1151+
1152+
const singleInstanceIssue = TriggerUtils.getSingleInstanceBlockIssue(blocks, blockType)
1153+
if (singleInstanceIssue) {
1154+
addNotification({
1155+
level: 'error',
1156+
message: `A workflow can only have one ${singleInstanceIssue.blockName} block. Please remove the existing one before adding a new one.`,
1157+
workflowId: activeWorkflowId || undefined,
1158+
})
1159+
return true
1160+
}
1161+
11501162
return false
11511163
},
11521164
[blocks, addNotification, activeWorkflowId]

apps/sim/blocks/blocks/response.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export const ResponseBlock: BlockConfig<ResponseBlockOutput> = {
1717
category: 'blocks',
1818
bgColor: '#2F55FF',
1919
icon: ResponseIcon,
20+
singleInstance: true,
2021
subBlocks: [
2122
{
2223
id: 'dataMode',

apps/sim/blocks/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,7 @@ export interface BlockConfig<T extends ToolResponse = ToolResponse> {
320320
subBlocks: SubBlockConfig[]
321321
triggerAllowed?: boolean
322322
authMode?: AuthMode
323+
singleInstance?: boolean
323324
tools: {
324325
access: string[]
325326
config?: {

apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { extractAndPersistCustomTools } from '@/lib/workflows/persistence/custom
1111
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
1212
import { isValidKey } from '@/lib/workflows/sanitization/key-validation'
1313
import { validateWorkflowState } from '@/lib/workflows/sanitization/validation'
14+
import { TriggerUtils } from '@/lib/workflows/triggers/triggers'
1415
import { getAllBlocks, getBlock } from '@/blocks/registry'
1516
import type { SubBlockConfig } from '@/blocks/types'
1617
import { EDGE, normalizeName } from '@/executor/constants'
@@ -62,6 +63,8 @@ type SkippedItemType =
6263
| 'invalid_subflow_parent'
6364
| 'nested_subflow_not_allowed'
6465
| 'duplicate_block_name'
66+
| 'duplicate_trigger'
67+
| 'duplicate_single_instance_block'
6568

6669
/**
6770
* Represents an item that was skipped during operation application
@@ -1775,6 +1778,34 @@ function applyOperationsToWorkflowState(
17751778
break
17761779
}
17771780

1781+
const triggerIssue = TriggerUtils.getTriggerAdditionIssue(modifiedState.blocks, params.type)
1782+
if (triggerIssue) {
1783+
logSkippedItem(skippedItems, {
1784+
type: 'duplicate_trigger',
1785+
operationType: 'add',
1786+
blockId: block_id,
1787+
reason: `Cannot add ${triggerIssue.triggerName} - a workflow can only have one`,
1788+
details: { requestedType: params.type, issue: triggerIssue.issue },
1789+
})
1790+
break
1791+
}
1792+
1793+
// Check single-instance block constraints (e.g., Response block)
1794+
const singleInstanceIssue = TriggerUtils.getSingleInstanceBlockIssue(
1795+
modifiedState.blocks,
1796+
params.type
1797+
)
1798+
if (singleInstanceIssue) {
1799+
logSkippedItem(skippedItems, {
1800+
type: 'duplicate_single_instance_block',
1801+
operationType: 'add',
1802+
blockId: block_id,
1803+
reason: `Cannot add ${singleInstanceIssue.blockName} - a workflow can only have one`,
1804+
details: { requestedType: params.type },
1805+
})
1806+
break
1807+
}
1808+
17781809
// Create new block with proper structure
17791810
const newBlock = createBlockFromParams(
17801811
block_id,

apps/sim/lib/workflows/triggers/triggers.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -592,4 +592,34 @@ export class TriggerUtils {
592592
const parentWithType = parent as T & { type?: string }
593593
return parentWithType.type === 'loop' || parentWithType.type === 'parallel'
594594
}
595+
596+
static isSingleInstanceBlockType(blockType: string): boolean {
597+
const blockConfig = getBlock(blockType)
598+
return blockConfig?.singleInstance === true
599+
}
600+
601+
static wouldViolateSingleInstanceBlock<T extends { type: string }>(
602+
blocks: T[] | Record<string, T>,
603+
blockType: string
604+
): boolean {
605+
if (!TriggerUtils.isSingleInstanceBlockType(blockType)) {
606+
return false
607+
}
608+
609+
const blockArray = Array.isArray(blocks) ? blocks : Object.values(blocks)
610+
return blockArray.some((block) => block.type === blockType)
611+
}
612+
613+
static getSingleInstanceBlockIssue<T extends { type: string }>(
614+
blocks: T[] | Record<string, T>,
615+
blockType: string
616+
): { issue: 'duplicate'; blockName: string } | null {
617+
if (!TriggerUtils.wouldViolateSingleInstanceBlock(blocks, blockType)) {
618+
return null
619+
}
620+
621+
const blockConfig = getBlock(blockType)
622+
const blockName = blockConfig?.name || blockType
623+
return { issue: 'duplicate', blockName }
624+
}
595625
}

apps/sim/stores/workflows/utils.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ export function getUniqueBlockName(baseName: string, existingBlocks: Record<stri
4141
return 'Start'
4242
}
4343

44+
if (normalizedBaseName === 'response') {
45+
return 'Response'
46+
}
47+
4448
const baseNameMatch = baseName.match(/^(.*?)(\s+\d+)?$/)
4549
const namePrefix = baseNameMatch ? baseNameMatch[1].trim() : baseName
4650

0 commit comments

Comments
 (0)