From 8e0b2dc49f93e89c195c4aaaad640bccd4af7923 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Mon, 20 Jul 2026 16:44:43 -0700 Subject: [PATCH 1/3] feat(blocks): surface deprecated block and model warnings on canvas --- .../w/[workflowId]/components/panel/panel.tsx | 7 ++- .../workflow-block/workflow-block.tsx | 57 ++++++++++++++++++ apps/sim/blocks/blocks/api_trigger.ts | 1 + apps/sim/blocks/blocks/chat_trigger.ts | 1 + apps/sim/blocks/blocks/confluence.ts | 2 + apps/sim/blocks/blocks/cursor.ts | 2 + apps/sim/blocks/blocks/extend.ts | 2 + apps/sim/blocks/blocks/file.ts | 5 ++ apps/sim/blocks/blocks/fireflies.ts | 2 + apps/sim/blocks/blocks/github.ts | 2 + apps/sim/blocks/blocks/gmail.ts | 2 + apps/sim/blocks/blocks/google_calendar.ts | 2 + apps/sim/blocks/blocks/google_sheets.ts | 1 + apps/sim/blocks/blocks/google_slides.ts | 2 + apps/sim/blocks/blocks/grain.ts | 2 + apps/sim/blocks/blocks/image_generator.ts | 1 + apps/sim/blocks/blocks/input_trigger.ts | 1 + apps/sim/blocks/blocks/intercom.ts | 2 + apps/sim/blocks/blocks/kalshi.ts | 2 + apps/sim/blocks/blocks/linear.ts | 2 + apps/sim/blocks/blocks/logs.ts | 1 + apps/sim/blocks/blocks/manual_trigger.ts | 1 + apps/sim/blocks/blocks/microsoft_excel.ts | 1 + apps/sim/blocks/blocks/mistral_parse.ts | 3 + apps/sim/blocks/blocks/notion.ts | 1 + apps/sim/blocks/blocks/pulse.ts | 2 + apps/sim/blocks/blocks/reducto.ts | 2 + apps/sim/blocks/blocks/router.ts | 1 + apps/sim/blocks/blocks/sharepoint.ts | 2 + apps/sim/blocks/blocks/starter.ts | 1 + apps/sim/blocks/blocks/stt.ts | 2 + apps/sim/blocks/blocks/textract.ts | 2 + apps/sim/blocks/blocks/video_generator.ts | 3 + apps/sim/blocks/blocks/vision.ts | 2 + apps/sim/blocks/blocks/workflow.ts | 1 + apps/sim/blocks/types.ts | 10 ++++ apps/sim/lib/posthog/events.ts | 6 ++ apps/sim/providers/models.test.ts | 59 +++++++++++++++++++ apps/sim/providers/models.ts | 28 +++++++++ apps/sim/scripts/check-block-registry.ts | 43 +++++++++++++- .../workflow-block/workflow-block-view.tsx | 31 ++++++++++ 41 files changed, 295 insertions(+), 5 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx index 5fa00fb44ca..1a997731fd8 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx @@ -458,10 +458,11 @@ export const Panel = memo(function Panel() { useEffect(() => { const handler = (e: Event) => { - const message = (e as CustomEvent).detail?.message - if (!message) return + const detail = (e as CustomEvent).detail + if (!detail?.message) return + e.preventDefault() setActiveTab('copilot') - copilotSendMessage(message) + copilotSendMessage(detail.message, undefined, detail.contexts) } window.addEventListener(MOTHERSHIP_SEND_MESSAGE_EVENT, handler) return () => window.removeEventListener(MOTHERSHIP_SEND_MESSAGE_EVENT, handler) 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 3e63939ba9e..937746ec097 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 @@ -3,11 +3,14 @@ import { createLogger } from '@sim/logger' import { SubBlockRowView, WorkflowBlockView } from '@sim/workflow-renderer' import { isEqual } from 'es-toolkit' import { useParams } from 'next/navigation' +import { usePostHog } from 'posthog-js/react' import { type NodeProps, useUpdateNodeInternals } from 'reactflow' import { useStoreWithEqualityFn } from 'zustand/traditional' import { getBaseUrl } from '@/lib/core/utils/urls' import { createMcpToolId } from '@/lib/mcp/shared' +import { sendMothershipMessage } from '@/lib/mothership/events' import { getProviderIdFromServiceId } from '@/lib/oauth' +import { captureEvent } from '@/lib/posthog/client' import { calculateWorkflowBlockDimensions } from '@/lib/workflows/blocks/deterministic-dimensions' import { getConditionRows, getRouterRows } from '@/lib/workflows/dynamic-handle-topology' import { @@ -45,6 +48,7 @@ import { import { useBlockVisual } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks' import { useBlockDimensions } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-dimensions' import { useCustomBlockOverlayVersion } from '@/blocks/custom/client-overlay' +import { getBlock } from '@/blocks/registry' import { SELECTOR_TYPES_HYDRATION_REQUIRED, type SubBlockConfig } from '@/blocks/types' import { getDependsOnFields } from '@/blocks/utils' import { useKnowledgeBase } from '@/hooks/kb/use-knowledge' @@ -58,6 +62,7 @@ import { useTablesList } from '@/hooks/queries/tables' import { useWorkflowMap } from '@/hooks/queries/workflows' import { useReactiveConditions } from '@/hooks/use-reactive-conditions' import { useSelectorDisplayName } from '@/hooks/use-selector-display-name' +import { getModelReplacement, isModelDeprecated } from '@/providers/models' import { useVariablesStore } from '@/stores/variables/store' import { useSubBlockStore } from '@/stores/workflows/subblock/store' import { useWorkflowStore } from '@/stores/workflows/workflow/store' @@ -479,6 +484,55 @@ export const WorkflowBlock = memo(function WorkflowBlock({ ), isEqual ) + + const posthog = usePostHog() + + const deprecation = useMemo(() => { + if (currentWorkflow.isDiffMode) return null + + const replacedBy = config.deprecated?.replacedBy + if (replacedBy) { + const target = getBlock(replacedBy) + if (!target) return null + const hasModel = config.subBlocks?.some((sub) => sub.id === 'model') + return { + kind: 'block' as const, + tooltip: 'This block is deprecated. Click to upgrade', + prompt: `The "${name}" block is deprecated. Migrate it to the current ${target.name} block: change the block type, then set the new block's required inputs as a separate edit (inputs are validated against the old type when sent in the same edit), or delete it and re-add ${target.name} and rewire the connections.${hasModel ? ' Also pick a current, non-deprecated model.' : ''}`, + } + } + + const model = blockSubBlockValues.model + if (typeof model === 'string' && isModelDeprecated(model)) { + if (!getModelReplacement(model)) return null + return { + kind: 'model' as const, + tooltip: `${model} is deprecated. Click to upgrade`, + prompt: `The "${name}" block uses the deprecated model "${model}". Switch it to the latest equivalent model.`, + } + } + + return null + }, [ + config.deprecated, + config.subBlocks, + name, + blockSubBlockValues.model, + currentWorkflow.isDiffMode, + ]) + + const onFixDeprecation = useCallback(() => { + if (!deprecation) return + captureEvent(posthog, 'deprecated_block_fix_clicked', { + block_type: type, + workflow_id: currentWorkflowId, + kind: deprecation.kind, + }) + sendMothershipMessage(deprecation.prompt, [ + { kind: 'workflow_block', workflowId: currentWorkflowId, blockId: id, label: name }, + ]) + }, [deprecation, posthog, type, currentWorkflowId, id, name]) + const canonicalIndex = useMemo(() => buildCanonicalIndex(config.subBlocks), [config.subBlocks]) const canonicalModeOverrides = currentStoreBlock?.data?.canonicalModes @@ -798,6 +852,9 @@ export const WorkflowBlock = memo(function WorkflowBlock({ deployChildWorkflow({ workflowId: childWorkflowId }) } }} + deprecationTooltip={deprecation?.tooltip} + canFixDeprecation={canEditWorkflow} + onFixDeprecation={onFixDeprecation} shouldShowScheduleBadge={shouldShowScheduleBadge} scheduleIsDisabled={Boolean(scheduleInfo?.isDisabled)} onReactivateSchedule={() => { diff --git a/apps/sim/blocks/blocks/api_trigger.ts b/apps/sim/blocks/blocks/api_trigger.ts index 59312c50498..dac480ad7d4 100644 --- a/apps/sim/blocks/blocks/api_trigger.ts +++ b/apps/sim/blocks/blocks/api_trigger.ts @@ -15,6 +15,7 @@ export const ApiTriggerBlock: BlockConfig = { `, category: 'triggers', hideFromToolbar: true, + deprecated: { replacedBy: 'start_trigger' }, bgColor: '#2F55FF', icon: ApiIcon, subBlocks: [ diff --git a/apps/sim/blocks/blocks/chat_trigger.ts b/apps/sim/blocks/blocks/chat_trigger.ts index 34fa5d0cce2..27294a135f6 100644 --- a/apps/sim/blocks/blocks/chat_trigger.ts +++ b/apps/sim/blocks/blocks/chat_trigger.ts @@ -16,6 +16,7 @@ export const ChatTriggerBlock: BlockConfig = { `, category: 'triggers', hideFromToolbar: true, + deprecated: { replacedBy: 'start_trigger' }, bgColor: '#6F3DFA', icon: ChatTriggerIcon, subBlocks: [], diff --git a/apps/sim/blocks/blocks/confluence.ts b/apps/sim/blocks/blocks/confluence.ts index 93b0dcda9ff..a7fbc14f0a3 100644 --- a/apps/sim/blocks/blocks/confluence.ts +++ b/apps/sim/blocks/blocks/confluence.ts @@ -12,6 +12,7 @@ export const ConfluenceBlock: BlockConfig = { name: 'Confluence (Legacy)', description: 'Interact with Confluence', hideFromToolbar: true, + deprecated: { replacedBy: 'confluence_v2' }, authMode: AuthMode.OAuth, longDescription: 'Integrate Confluence into the workflow. Can read, create, update, delete pages, manage comments, attachments, labels, and search content.', @@ -360,6 +361,7 @@ export const ConfluenceBlock: BlockConfig = { export const ConfluenceV2Block: BlockConfig = { ...ConfluenceBlock, + deprecated: undefined, type: 'confluence_v2', name: 'Confluence', hideFromToolbar: false, diff --git a/apps/sim/blocks/blocks/cursor.ts b/apps/sim/blocks/blocks/cursor.ts index 7383268e37f..1f31a6bcf56 100644 --- a/apps/sim/blocks/blocks/cursor.ts +++ b/apps/sim/blocks/blocks/cursor.ts @@ -17,6 +17,7 @@ export const CursorBlock: BlockConfig = { icon: CursorIcon, authMode: AuthMode.ApiKey, hideFromToolbar: true, + deprecated: { replacedBy: 'cursor_v2' }, subBlocks: [ { id: 'operation', @@ -223,6 +224,7 @@ export const CursorBlock: BlockConfig = { export const CursorV2Block: BlockConfig = { ...CursorBlock, + deprecated: undefined, type: 'cursor_v2', name: 'Cursor', description: 'Launch and manage Cursor cloud agents to work on GitHub repositories', diff --git a/apps/sim/blocks/blocks/extend.ts b/apps/sim/blocks/blocks/extend.ts index 34667a5a0c6..f06a853df3f 100644 --- a/apps/sim/blocks/blocks/extend.ts +++ b/apps/sim/blocks/blocks/extend.ts @@ -14,6 +14,7 @@ export const ExtendBlock: BlockConfig = { name: 'Extend', description: 'Parse and extract content from documents', hideFromToolbar: true, + deprecated: { replacedBy: 'extend_v2' }, authMode: AuthMode.ApiKey, longDescription: 'Integrate Extend AI into the workflow. Parse and extract structured content from documents including PDFs, images, and Office files.', @@ -165,6 +166,7 @@ const extendV2SubBlocks = (ExtendBlock.subBlocks || []).flatMap((subBlock) => { export const ExtendV2Block: BlockConfig = { ...ExtendBlock, + deprecated: undefined, type: 'extend_v2', name: 'Extend', hideFromToolbar: false, diff --git a/apps/sim/blocks/blocks/file.ts b/apps/sim/blocks/blocks/file.ts index 49ea1361a8d..52a92261267 100644 --- a/apps/sim/blocks/blocks/file.ts +++ b/apps/sim/blocks/blocks/file.ts @@ -78,6 +78,7 @@ export const FileBlock: BlockConfig = { bgColor: '#40916C', icon: DocumentIcon, hideFromToolbar: true, + deprecated: { replacedBy: 'file_v5' }, subBlocks: [ { id: 'inputMethod', @@ -185,6 +186,7 @@ export const FileV2Block: BlockConfig = { name: 'File (Legacy)', description: 'Read and parse multiple files', hideFromToolbar: true, + deprecated: { replacedBy: 'file_v5' }, subBlocks: [ { id: 'file', @@ -278,6 +280,7 @@ export const FileV3Block: BlockConfig = { bgColor: '#40916C', icon: DocumentIcon, hideFromToolbar: true, + deprecated: { replacedBy: 'file_v5' }, subBlocks: [ { id: 'operation', @@ -568,6 +571,7 @@ export const FileV4Block: BlockConfig = { longDescription: 'Read workspace files by picker or canonical ID, fetch and parse files from URLs with optional headers, write new workspace files, or append content to existing files.', hideFromToolbar: true, + deprecated: { replacedBy: 'file_v5' }, bestPractices: ` - Use Read when you need an existing workspace file object by picker selection or canonical file ID. - Use Fetch for external file URLs. Add headers for authenticated downloads, for example Slack private file URLs require an Authorization Bearer token. @@ -820,6 +824,7 @@ export const FileV4Block: BlockConfig = { export const FileV5Block: BlockConfig = { ...FileV4Block, + deprecated: undefined, type: 'file_v5', name: 'File', description: diff --git a/apps/sim/blocks/blocks/fireflies.ts b/apps/sim/blocks/blocks/fireflies.ts index ddfefa7ddd5..9d4adfb587a 100644 --- a/apps/sim/blocks/blocks/fireflies.ts +++ b/apps/sim/blocks/blocks/fireflies.ts @@ -11,6 +11,7 @@ export const FirefliesBlock: BlockConfig = { name: 'Fireflies (Legacy)', description: 'Interact with Fireflies.ai meeting transcripts and recordings', hideFromToolbar: true, + deprecated: { replacedBy: 'fireflies_v2' }, authMode: AuthMode.ApiKey, triggerAllowed: true, longDescription: @@ -646,6 +647,7 @@ const firefliesV2Inputs = FirefliesBlock.inputs export const FirefliesV2Block: BlockConfig = { ...FirefliesBlock, + deprecated: undefined, type: 'fireflies_v2', name: 'Fireflies', description: 'Interact with Fireflies.ai meeting transcripts and recordings', diff --git a/apps/sim/blocks/blocks/github.ts b/apps/sim/blocks/blocks/github.ts index ab5a4c928e1..706c5c58bc4 100644 --- a/apps/sim/blocks/blocks/github.ts +++ b/apps/sim/blocks/blocks/github.ts @@ -20,6 +20,7 @@ export const GitHubBlock: BlockConfig = { icon: GithubIcon, triggerAllowed: true, hideFromToolbar: true, + deprecated: { replacedBy: 'github_v2' }, subBlocks: [ { id: 'operation', @@ -2102,6 +2103,7 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, export const GitHubV2Block: BlockConfig = { ...GitHubBlock, + deprecated: undefined, type: 'github_v2', name: 'GitHub', hideFromToolbar: false, diff --git a/apps/sim/blocks/blocks/gmail.ts b/apps/sim/blocks/blocks/gmail.ts index fc4761d5c7f..ab7535e4165 100644 --- a/apps/sim/blocks/blocks/gmail.ts +++ b/apps/sim/blocks/blocks/gmail.ts @@ -57,6 +57,7 @@ export const GmailBlock: BlockConfig = { bgColor: '#FFFFFF', icon: GmailIcon, hideFromToolbar: true, + deprecated: { replacedBy: 'gmail_v2' }, triggerAllowed: true, subBlocks: [ // Operation selector @@ -575,6 +576,7 @@ Return ONLY the search query - no explanations, no extra text.`, export const GmailV2Block: BlockConfig = { ...GmailBlock, + deprecated: undefined, type: 'gmail_v2', name: 'Gmail', hideFromToolbar: false, diff --git a/apps/sim/blocks/blocks/google_calendar.ts b/apps/sim/blocks/blocks/google_calendar.ts index e967f52a35f..c4e9919a16a 100644 --- a/apps/sim/blocks/blocks/google_calendar.ts +++ b/apps/sim/blocks/blocks/google_calendar.ts @@ -19,6 +19,7 @@ export const GoogleCalendarBlock: BlockConfig = { bgColor: '#FFFFFF', icon: GoogleCalendarIcon, hideFromToolbar: true, + deprecated: { replacedBy: 'google_calendar_v2' }, subBlocks: [ { id: 'operation', @@ -925,6 +926,7 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, export const GoogleCalendarV2Block: BlockConfig = { ...GoogleCalendarBlock, + deprecated: undefined, type: 'google_calendar_v2', name: 'Google Calendar', hideFromToolbar: false, diff --git a/apps/sim/blocks/blocks/google_sheets.ts b/apps/sim/blocks/blocks/google_sheets.ts index 18397fbe736..4ef9ff8b7ad 100644 --- a/apps/sim/blocks/blocks/google_sheets.ts +++ b/apps/sim/blocks/blocks/google_sheets.ts @@ -13,6 +13,7 @@ export const GoogleSheetsBlock: BlockConfig = { description: 'Read, write, and update data', authMode: AuthMode.OAuth, hideFromToolbar: true, + deprecated: { replacedBy: 'google_sheets_v2' }, longDescription: 'Integrate Google Sheets into the workflow. Can read, write, append, and update data.', docsLink: 'https://docs.sim.ai/integrations/google_sheets', diff --git a/apps/sim/blocks/blocks/google_slides.ts b/apps/sim/blocks/blocks/google_slides.ts index b05ecf8fde9..26a8a100723 100644 --- a/apps/sim/blocks/blocks/google_slides.ts +++ b/apps/sim/blocks/blocks/google_slides.ts @@ -11,6 +11,7 @@ export const GoogleSlidesBlock: BlockConfig = { name: 'Google Slides (Legacy)', description: 'Read, write, and create presentations', hideFromToolbar: true, + deprecated: { replacedBy: 'google_slides_v2' }, authMode: AuthMode.OAuth, longDescription: 'Build, edit, and export branded Google Slides presentations end-to-end. Copy a template, replace text and image tokens, embed Sheets charts, style text and shapes with brand fonts and colors, manage tables and layouts, group elements, run atomic batch updates, and export to PDF or PPTX.', @@ -3454,6 +3455,7 @@ const googleSlidesV2Inputs = GoogleSlidesBlock.inputs export const GoogleSlidesV2Block: BlockConfig = { ...GoogleSlidesBlock, + deprecated: undefined, type: 'google_slides_v2', name: 'Google Slides', description: 'Read, write, and create presentations', diff --git a/apps/sim/blocks/blocks/grain.ts b/apps/sim/blocks/blocks/grain.ts index 4e4bf8e35a9..14bf0eb3bd7 100644 --- a/apps/sim/blocks/blocks/grain.ts +++ b/apps/sim/blocks/blocks/grain.ts @@ -27,6 +27,7 @@ export const GrainBlock: BlockConfig = { // Superseded by grain_v2 (Grain API v1 sunsets 2026-09-07); existing blocks // keep rendering, new blocks come from the v2 entry. hideFromToolbar: true, + deprecated: { replacedBy: 'grain_v2' }, longDescription: 'Integrate Grain into your workflow. Access meeting recordings, transcripts, highlights, and AI-generated summaries. Can also trigger workflows based on Grain webhook events.', category: 'tools', @@ -501,6 +502,7 @@ Return ONLY the search term - no explanations, no quotes, no extra text.`, */ export const GrainV2Block: BlockConfig = { ...GrainBlock, + deprecated: undefined, type: 'grain_v2', hideFromToolbar: false, subBlocks: [ diff --git a/apps/sim/blocks/blocks/image_generator.ts b/apps/sim/blocks/blocks/image_generator.ts index dbeb940d47b..f47b14b56f7 100644 --- a/apps/sim/blocks/blocks/image_generator.ts +++ b/apps/sim/blocks/blocks/image_generator.ts @@ -57,6 +57,7 @@ export const ImageGeneratorBlock: BlockConfig = { name: 'Image Generator', description: 'Generate images', hideFromToolbar: true, + deprecated: { replacedBy: 'image_generator_v2' }, authMode: AuthMode.ApiKey, longDescription: 'Integrate Image Generator into the workflow. Can generate images using DALL-E 3 and GPT Image models.', diff --git a/apps/sim/blocks/blocks/input_trigger.ts b/apps/sim/blocks/blocks/input_trigger.ts index 51de24d6e86..beab6a48f12 100644 --- a/apps/sim/blocks/blocks/input_trigger.ts +++ b/apps/sim/blocks/blocks/input_trigger.ts @@ -19,6 +19,7 @@ export const InputTriggerBlock: BlockConfig = { `, category: 'triggers', hideFromToolbar: true, + deprecated: { replacedBy: 'start_trigger' }, bgColor: '#3B82F6', icon: InputTriggerIcon, subBlocks: [ diff --git a/apps/sim/blocks/blocks/intercom.ts b/apps/sim/blocks/blocks/intercom.ts index 56d101484ee..4ab717db772 100644 --- a/apps/sim/blocks/blocks/intercom.ts +++ b/apps/sim/blocks/blocks/intercom.ts @@ -8,6 +8,7 @@ export const IntercomBlock: BlockConfig = { type: 'intercom', name: 'Intercom (Legacy)', hideFromToolbar: true, + deprecated: { replacedBy: 'intercom_v2' }, description: 'Manage contacts, companies, conversations, tickets, and messages in Intercom', longDescription: 'Integrate Intercom into the workflow. Can create, get, update, list, search, and delete contacts; create, get, and list companies; get, list, reply, and search conversations; create and get tickets; and create messages.', @@ -1404,6 +1405,7 @@ Return ONLY the numeric timestamp.`, export const IntercomV2Block: BlockConfig = { ...IntercomBlock, + deprecated: undefined, type: 'intercom_v2', name: 'Intercom', integrationType: IntegrationType.Support, diff --git a/apps/sim/blocks/blocks/kalshi.ts b/apps/sim/blocks/blocks/kalshi.ts index 527bd35941e..31ae5cae1b2 100644 --- a/apps/sim/blocks/blocks/kalshi.ts +++ b/apps/sim/blocks/blocks/kalshi.ts @@ -14,6 +14,7 @@ export const KalshiBlock: BlockConfig = { category: 'tools', integrationType: IntegrationType.Analytics, hideFromToolbar: true, + deprecated: { replacedBy: 'kalshi_v2' }, bgColor: '#09C285', iconColor: '#09C285', icon: KalshiIcon, @@ -762,6 +763,7 @@ Return ONLY the numeric timestamp (seconds since Unix epoch) - no explanations, export const KalshiV2Block: BlockConfig = { ...KalshiBlock, + deprecated: undefined, type: 'kalshi_v2', name: 'Kalshi', description: 'Access prediction markets and trade on Kalshi', diff --git a/apps/sim/blocks/blocks/linear.ts b/apps/sim/blocks/blocks/linear.ts index 809dadc786d..fd8249c2343 100644 --- a/apps/sim/blocks/blocks/linear.ts +++ b/apps/sim/blocks/blocks/linear.ts @@ -11,6 +11,7 @@ export const LinearBlock: BlockConfig = { name: 'Linear (Legacy)', description: 'Interact with Linear issues, projects, and more', hideFromToolbar: true, + deprecated: { replacedBy: 'linear_v2' }, authMode: AuthMode.OAuth, triggerAllowed: true, longDescription: @@ -2561,6 +2562,7 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n */ export const LinearV2Block: BlockConfig = { ...LinearBlock, + deprecated: undefined, type: 'linear_v2', name: 'Linear', hideFromToolbar: false, diff --git a/apps/sim/blocks/blocks/logs.ts b/apps/sim/blocks/blocks/logs.ts index 75ab5f54f04..0c5c88d8a6f 100644 --- a/apps/sim/blocks/blocks/logs.ts +++ b/apps/sim/blocks/blocks/logs.ts @@ -6,6 +6,7 @@ export const LogsBlock: BlockConfig = { type: 'logs', name: 'Logs', hideFromToolbar: true, + deprecated: { replacedBy: 'logs_v2' }, description: 'Query workflow execution logs', longDescription: 'Search workflow execution logs in the current workspace, fetch a single log by id, or load full execution details with the per-block state snapshot.', diff --git a/apps/sim/blocks/blocks/manual_trigger.ts b/apps/sim/blocks/blocks/manual_trigger.ts index ded5616efb9..1ceb98b559b 100644 --- a/apps/sim/blocks/blocks/manual_trigger.ts +++ b/apps/sim/blocks/blocks/manual_trigger.ts @@ -18,6 +18,7 @@ export const ManualTriggerBlock: BlockConfig = { `, category: 'triggers', hideFromToolbar: true, + deprecated: { replacedBy: 'start_trigger' }, bgColor: '#2563EB', icon: ManualTriggerIcon, subBlocks: [], diff --git a/apps/sim/blocks/blocks/microsoft_excel.ts b/apps/sim/blocks/blocks/microsoft_excel.ts index 4312a5b370a..2143c016b86 100644 --- a/apps/sim/blocks/blocks/microsoft_excel.ts +++ b/apps/sim/blocks/blocks/microsoft_excel.ts @@ -60,6 +60,7 @@ export const MicrosoftExcelBlock: BlockConfig = { description: 'Read, write, and update data', authMode: AuthMode.OAuth, hideFromToolbar: true, + deprecated: { replacedBy: 'microsoft_excel_v2' }, longDescription: 'Integrate Microsoft Excel into the workflow. Can read, write, update, add to table, and create new worksheets.', docsLink: 'https://docs.sim.ai/integrations/microsoft_excel', diff --git a/apps/sim/blocks/blocks/mistral_parse.ts b/apps/sim/blocks/blocks/mistral_parse.ts index a182ef8b2ec..4025be7a6e6 100644 --- a/apps/sim/blocks/blocks/mistral_parse.ts +++ b/apps/sim/blocks/blocks/mistral_parse.ts @@ -15,6 +15,7 @@ export const MistralParseBlock: BlockConfig = { name: 'Mistral Parser (Legacy)', description: 'Extract text from PDF documents', hideFromToolbar: true, + deprecated: { replacedBy: 'mistral_parse_v3' }, authMode: AuthMode.ApiKey, longDescription: `Integrate Mistral Parse into the workflow. Can extract text from uploaded PDF documents, or from a URL.`, docsLink: 'https://docs.sim.ai/integrations/mistral_parse', @@ -161,6 +162,7 @@ export const MistralParseV2Block: BlockConfig = { name: 'Mistral Parser', description: 'Extract text from PDF documents', hideFromToolbar: true, + deprecated: { replacedBy: 'mistral_parse_v3' }, subBlocks: [ { id: 'fileUpload', @@ -287,6 +289,7 @@ export const MistralParseV2Block: BlockConfig = { */ export const MistralParseV3Block: BlockConfig = { ...MistralParseBlock, + deprecated: undefined, type: 'mistral_parse_v3', name: 'Mistral Parser', description: 'Extract text from PDF documents', diff --git a/apps/sim/blocks/blocks/notion.ts b/apps/sim/blocks/blocks/notion.ts index db2fecf3e09..cdce1b45ec8 100644 --- a/apps/sim/blocks/blocks/notion.ts +++ b/apps/sim/blocks/blocks/notion.ts @@ -12,6 +12,7 @@ export const NotionBlock: BlockConfig = { type: 'notion', name: 'Notion (Legacy)', hideFromToolbar: true, + deprecated: { replacedBy: 'notion_v2' }, description: 'Manage Notion pages', authMode: AuthMode.OAuth, longDescription: diff --git a/apps/sim/blocks/blocks/pulse.ts b/apps/sim/blocks/blocks/pulse.ts index f16ff5d4441..7bcaa364b88 100644 --- a/apps/sim/blocks/blocks/pulse.ts +++ b/apps/sim/blocks/blocks/pulse.ts @@ -14,6 +14,7 @@ export const PulseBlock: BlockConfig = { name: 'Pulse', description: 'Extract text from documents using Pulse OCR', hideFromToolbar: true, + deprecated: { replacedBy: 'pulse_v2' }, authMode: AuthMode.ApiKey, longDescription: 'Integrate Pulse into the workflow. Extract text from PDF documents, images, and Office files via URL or upload.', @@ -162,6 +163,7 @@ const pulseV2SubBlocks = (PulseBlock.subBlocks || []).flatMap((subBlock) => { export const PulseV2Block: BlockConfig = { ...PulseBlock, + deprecated: undefined, type: 'pulse_v2', name: 'Pulse', hideFromToolbar: false, diff --git a/apps/sim/blocks/blocks/reducto.ts b/apps/sim/blocks/blocks/reducto.ts index 521b50b5495..02bf3ff02be 100644 --- a/apps/sim/blocks/blocks/reducto.ts +++ b/apps/sim/blocks/blocks/reducto.ts @@ -15,6 +15,7 @@ export const ReductoBlock: BlockConfig = { name: 'Reducto', description: 'Extract text from PDF documents', hideFromToolbar: true, + deprecated: { replacedBy: 'reducto_v2' }, authMode: AuthMode.ApiKey, longDescription: `Integrate Reducto Parse into the workflow. Can extract text from uploaded PDF documents, or from a URL.`, docsLink: 'https://docs.sim.ai/integrations/reducto', @@ -168,6 +169,7 @@ const reductoV2SubBlocks = (ReductoBlock.subBlocks || []).flatMap((subBlock) => export const ReductoV2Block: BlockConfig = { ...ReductoBlock, + deprecated: undefined, type: 'reducto_v2', name: 'Reducto', hideFromToolbar: false, diff --git a/apps/sim/blocks/blocks/router.ts b/apps/sim/blocks/blocks/router.ts index 15835f9b8af..76d156e776c 100644 --- a/apps/sim/blocks/blocks/router.ts +++ b/apps/sim/blocks/blocks/router.ts @@ -157,6 +157,7 @@ export const RouterBlock: BlockConfig = { bgColor: '#28C43F', icon: ConnectIcon, hideFromToolbar: true, // Hide legacy version from toolbar + deprecated: { replacedBy: 'router_v2' }, subBlocks: [ { id: 'prompt', diff --git a/apps/sim/blocks/blocks/sharepoint.ts b/apps/sim/blocks/blocks/sharepoint.ts index d73eb1adf09..b19e5c84c89 100644 --- a/apps/sim/blocks/blocks/sharepoint.ts +++ b/apps/sim/blocks/blocks/sharepoint.ts @@ -15,6 +15,7 @@ export const SharepointBlock: BlockConfig = { description: 'Work with pages and lists', authMode: AuthMode.OAuth, hideFromToolbar: true, + deprecated: { replacedBy: 'sharepoint_v2' }, longDescription: 'Integrate SharePoint into the workflow. Read/create pages, list sites, and work with lists (read, create, update items). Requires OAuth.', docsLink: 'https://docs.sim.ai/integrations/sharepoint', @@ -821,6 +822,7 @@ const SHAREPOINT_V2_PAGE_MUTATION_OPERATIONS = [ export const SharepointV2Block: BlockConfig = { ...SharepointBlock, + deprecated: undefined, type: 'sharepoint_v2', name: 'SharePoint', hideFromToolbar: false, diff --git a/apps/sim/blocks/blocks/starter.ts b/apps/sim/blocks/blocks/starter.ts index 7e5395889f6..00196da3f24 100644 --- a/apps/sim/blocks/blocks/starter.ts +++ b/apps/sim/blocks/blocks/starter.ts @@ -10,6 +10,7 @@ export const StarterBlock: BlockConfig = { bgColor: '#2FB3FF', icon: StartIcon, hideFromToolbar: true, + deprecated: { replacedBy: 'start_trigger' }, subBlocks: [ // Main trigger selector { diff --git a/apps/sim/blocks/blocks/stt.ts b/apps/sim/blocks/blocks/stt.ts index c92446598eb..0d75264dc87 100644 --- a/apps/sim/blocks/blocks/stt.ts +++ b/apps/sim/blocks/blocks/stt.ts @@ -8,6 +8,7 @@ export const SttBlock: BlockConfig = { name: 'Speech-to-Text', description: 'Convert speech to text using AI', hideFromToolbar: true, + deprecated: { replacedBy: 'stt_v2' }, authMode: AuthMode.ApiKey, longDescription: 'Transcribe audio and video files to text using leading AI providers. Supports multiple languages, timestamps, and speaker diarization.', @@ -361,6 +362,7 @@ const sttV2SubBlocks = (SttBlock.subBlocks || []).filter((subBlock) => subBlock. export const SttV2Block: BlockConfig = { ...SttBlock, + deprecated: undefined, type: 'stt_v2', name: 'Speech-to-Text', hideFromToolbar: false, diff --git a/apps/sim/blocks/blocks/textract.ts b/apps/sim/blocks/blocks/textract.ts index c5a87c480ab..8cef450ae3c 100644 --- a/apps/sim/blocks/blocks/textract.ts +++ b/apps/sim/blocks/blocks/textract.ts @@ -14,6 +14,7 @@ export const TextractBlock: BlockConfig = { name: 'AWS Textract', description: 'Extract text, tables, and forms from documents', hideFromToolbar: true, + deprecated: { replacedBy: 'textract_v2' }, authMode: AuthMode.ApiKey, longDescription: `Integrate AWS Textract into your workflow to extract text, tables, forms, and key-value pairs from documents. Single-page mode supports JPEG, PNG, and single-page PDF. Multi-page mode supports multi-page PDF and TIFF.`, docsLink: 'https://docs.sim.ai/integrations/textract', @@ -238,6 +239,7 @@ function requireAwsCredentials(params: Record) { export const TextractV2Block: BlockConfig = { ...TextractBlock, + deprecated: undefined, type: 'textract_v2', name: 'AWS Textract', hideFromToolbar: false, diff --git a/apps/sim/blocks/blocks/video_generator.ts b/apps/sim/blocks/blocks/video_generator.ts index 496738dc0d8..cbd933bd8ba 100644 --- a/apps/sim/blocks/blocks/video_generator.ts +++ b/apps/sim/blocks/blocks/video_generator.ts @@ -75,6 +75,7 @@ export const VideoGeneratorBlock: BlockConfig = { name: 'Video Generator (Legacy)', description: 'Generate videos from text using AI', hideFromToolbar: true, + deprecated: { replacedBy: 'video_generator_v3' }, authMode: AuthMode.ApiKey, longDescription: 'Generate high-quality videos from text prompts using leading AI providers. Supports multiple models, aspect ratios, resolutions, and provider-specific features like world consistency, camera controls, and audio generation.', @@ -892,6 +893,7 @@ export const VideoGeneratorV2Block: BlockConfig = { type: 'video_generator_v2', name: 'Video Generator', hideFromToolbar: true, + deprecated: { replacedBy: 'video_generator_v3' }, subBlocks: [ { id: 'provider', @@ -1649,6 +1651,7 @@ export const VideoGeneratorV2Block: BlockConfig = { export const VideoGeneratorV3Block: BlockConfig = { ...VideoGeneratorV2Block, + deprecated: undefined, type: 'video_generator_v3', name: 'Video Generator', description: 'Generate videos from text using AI', diff --git a/apps/sim/blocks/blocks/vision.ts b/apps/sim/blocks/blocks/vision.ts index 16e3b3aaa39..d7cb6d0ffb1 100644 --- a/apps/sim/blocks/blocks/vision.ts +++ b/apps/sim/blocks/blocks/vision.ts @@ -26,6 +26,7 @@ export const VisionBlock: BlockConfig = { name: 'Vision (Legacy)', description: 'Analyze images with vision models', hideFromToolbar: true, + deprecated: {}, authMode: AuthMode.ApiKey, longDescription: 'Integrate Vision into the workflow. Can analyze images with vision models.', docsLink: 'https://docs.sim.ai/integrations/vision', @@ -109,6 +110,7 @@ export const VisionV2Block: BlockConfig = { name: 'Vision', description: 'Analyze images with vision models', hideFromToolbar: true, + deprecated: {}, tools: { access: ['vision_tool_v2'], config: { diff --git a/apps/sim/blocks/blocks/workflow.ts b/apps/sim/blocks/blocks/workflow.ts index 667b6614ea4..1aa6cfba87a 100644 --- a/apps/sim/blocks/blocks/workflow.ts +++ b/apps/sim/blocks/blocks/workflow.ts @@ -64,4 +64,5 @@ export const WorkflowBlock: BlockConfig = { }, }, hideFromToolbar: true, + deprecated: { replacedBy: 'workflow_input' }, } diff --git a/apps/sim/blocks/types.ts b/apps/sim/blocks/types.ts index 9024e44cb88..ac202046a38 100644 --- a/apps/sim/blocks/types.ts +++ b/apps/sim/blocks/types.ts @@ -496,6 +496,16 @@ export interface BlockConfig { * gated. Remove at GA. */ preview?: boolean + /** + * Marks a superseded block. Placed instances keep executing and rendering; the + * canvas surfaces a deprecation badge and downstream jobs may notify owners. + * `replacedBy` is the block `type` users should migrate to — omit when no + * direct successor exists. Distinct from {@link hideFromToolbar} (a rendering + * decision) and {@link preview} (unreleased). Remove config at end-of-life. + */ + deprecated?: { + replacedBy?: string + } triggers?: { enabled: boolean available: string[] // List of trigger IDs this block supports diff --git a/apps/sim/lib/posthog/events.ts b/apps/sim/lib/posthog/events.ts index dcd47d895e2..c6d0a8f09ea 100644 --- a/apps/sim/lib/posthog/events.ts +++ b/apps/sim/lib/posthog/events.ts @@ -234,6 +234,12 @@ export interface PostHogEventMap { workflow_id: string } + deprecated_block_fix_clicked: { + block_type: string + workflow_id: string + kind: 'block' | 'model' + } + knowledge_base_created: { knowledge_base_id: string workspace_id: string diff --git a/apps/sim/providers/models.test.ts b/apps/sim/providers/models.test.ts index 630da93fb2e..c7028a3f4ef 100644 --- a/apps/sim/providers/models.test.ts +++ b/apps/sim/providers/models.test.ts @@ -5,10 +5,32 @@ import { describe, expect, it } from 'vitest' import { getBaseModelProviders, getHostedModels, + getModelReplacement, + isModelDeprecated, orderModelIdsByReleaseDate, PROVIDER_DEFINITIONS, } from '@/providers/models' +const DYNAMIC_PROVIDERS = new Set([ + 'ollama', + 'ollama-cloud', + 'vllm', + 'litellm', + 'openrouter', + 'fireworks', + 'together', + 'baseten', +]) + +function firstDeprecatedModelId(): string | undefined { + for (const [providerId, provider] of Object.entries(PROVIDER_DEFINITIONS)) { + if (DYNAMIC_PROVIDERS.has(providerId)) continue + const dep = provider.models.find((m) => m.deprecated) + if (dep) return dep.id + } + return undefined +} + /** Maps a lowercased model ID to its provider's index in the catalog. */ const PROVIDER_INDEX_BY_MODEL = new Map() /** Maps a lowercased model ID to its release time (ms), or null when undated. */ @@ -294,3 +316,40 @@ describe('xai provider definition', () => { expect(getHostedModels()).toContain('grok-4.5') }) }) + +describe('isModelDeprecated', () => { + it('returns true for a catalogued deprecated model (case-insensitive)', () => { + const id = firstDeprecatedModelId() + expect(id).toBeDefined() + expect(isModelDeprecated(id!)).toBe(true) + expect(isModelDeprecated(id!.toUpperCase())).toBe(true) + }) + + it('returns false for the default model of every provider', () => { + for (const provider of Object.values(PROVIDER_DEFINITIONS)) { + if (provider.defaultModel) expect(isModelDeprecated(provider.defaultModel)).toBe(false) + } + }) + + it('returns false for empty, unknown, and dynamic-provider ids', () => { + expect(isModelDeprecated('')).toBe(false) + expect(isModelDeprecated(undefined)).toBe(false) + expect(isModelDeprecated(null)).toBe(false) + expect(isModelDeprecated('not-a-real-model')).toBe(false) + expect(isModelDeprecated('openrouter/some/model')).toBe(false) + }) +}) + +describe('getModelReplacement', () => { + it('returns a non-deprecated replacement for a deprecated model', () => { + const id = firstDeprecatedModelId()! + const replacement = getModelReplacement(id) + if (replacement !== null) { + expect(isModelDeprecated(replacement)).toBe(false) + } + }) + + it('returns null for a non-deprecated model', () => { + expect(getModelReplacement('claude-sonnet-5')).toBeNull() + }) +}) diff --git a/apps/sim/providers/models.ts b/apps/sim/providers/models.ts index ae5e201c8e2..5ccda8e6043 100644 --- a/apps/sim/providers/models.ts +++ b/apps/sim/providers/models.ts @@ -3850,6 +3850,34 @@ export function isKnownModelId(modelId: string): boolean { return false } +const DEPRECATED_STATIC_MODELS = new Map() +for (const [providerId, provider] of Object.entries(PROVIDER_DEFINITIONS)) { + if ((DYNAMIC_MODEL_PROVIDERS as readonly string[]).includes(providerId)) continue + for (const model of provider.models) { + if (model.deprecated) DEPRECATED_STATIC_MODELS.set(model.id.toLowerCase(), { providerId }) + } +} + +/** + * Whether a stored model id is a deprecated static-catalog model. Dynamic-provider + * and unknown ids are never deprecated (they carry no static catalog entry). + */ +export function isModelDeprecated(modelId: string | undefined | null): boolean { + return !!modelId && DEPRECATED_STATIC_MODELS.has(modelId.toLowerCase()) +} + +/** + * The current model a deprecated model should migrate to — its provider's + * default, when that default is itself non-deprecated. `null` when no clean + * target exists, in which case the model is not surfaced as fixable. + */ +export function getModelReplacement(modelId: string): string | null { + const entry = DEPRECATED_STATIC_MODELS.get(modelId.toLowerCase()) + if (!entry) return null + const def = PROVIDER_DEFINITIONS[entry.providerId]?.defaultModel + return def && !isModelDeprecated(def) ? def : null +} + function getRecommendedModels(): string[] { const models: string[] = [] for (const [providerId, provider] of Object.entries(PROVIDER_DEFINITIONS)) { diff --git a/apps/sim/scripts/check-block-registry.ts b/apps/sim/scripts/check-block-registry.ts index 6a8421e41a7..1f0a7a980b7 100644 --- a/apps/sim/scripts/check-block-registry.ts +++ b/apps/sim/scripts/check-block-registry.ts @@ -26,7 +26,7 @@ import { execSync } from 'child_process' import { SUBBLOCK_ID_MIGRATIONS } from '@/lib/workflows/migrations/subblock-migrations' -import { getAllBlocks, getBlockMeta } from '@/blocks/registry' +import { getAllBlocks, getBlock, getBlockMeta } from '@/blocks/registry' import { tools as toolRegistry } from '@/tools/registry' const baseRef = process.argv[2] || 'HEAD~1' @@ -280,6 +280,38 @@ function checkIntegrationMetaCoverage(): CheckResult { return { kind: 'fail', errors } } +function checkDeprecationReplacedBy(): CheckResult { + const errors: string[] = [] + + for (const block of getAllBlocks()) { + const replacedBy = block.deprecated?.replacedBy + if (!replacedBy) continue + + const target = getBlock(replacedBy) + if (!target) { + errors.push( + `Block "${block.type}" is deprecated with replacedBy: '${replacedBy}', but no such block exists.` + ) + continue + } + if (target.deprecated) { + errors.push( + `Block "${block.type}" points replacedBy: '${replacedBy}', but that block is itself deprecated.` + ) + } + if (target.preview) { + errors.push( + `Block "${block.type}" points replacedBy: '${replacedBy}', but that block is preview (not GA).` + ) + } + } + + if (errors.length === 0) { + return { kind: 'pass', message: 'Deprecation replacedBy check passed' } + } + return { kind: 'fail', errors } +} + function reportResult(label: string, failureHeader: string, result: CheckResult): boolean { if (result.kind === 'pass') { console.log(`✓ ${result.message}`) @@ -300,6 +332,7 @@ function reportResult(label: string, failureHeader: string, result: CheckResult) const stabilityResult = checkSubblockIdStability() const canonicalResult = checkCanonicalIdContract() const metaCoverageResult = checkIntegrationMetaCoverage() +const deprecationResult = checkDeprecationReplacedBy() const stabilityOk = reportResult( 'Subblock ID stability check', @@ -319,4 +352,10 @@ const metaCoverageOk = reportResult( metaCoverageResult ) -process.exit(stabilityOk && canonicalOk && metaCoverageOk ? 0 : 1) +const deprecationOk = reportResult( + 'Deprecation replacedBy check', + 'A deprecated block must point replacedBy at a real, GA, non-deprecated successor.', + deprecationResult +) + +process.exit(stabilityOk && canonicalOk && metaCoverageOk && deprecationOk ? 0 : 1) diff --git a/packages/workflow-renderer/src/workflow-block/workflow-block-view.tsx b/packages/workflow-renderer/src/workflow-block/workflow-block-view.tsx index cf43c63796d..ff4b75d649e 100644 --- a/packages/workflow-renderer/src/workflow-block/workflow-block-view.tsx +++ b/packages/workflow-renderer/src/workflow-block/workflow-block-view.tsx @@ -70,6 +70,12 @@ export interface WorkflowBlockViewProps { /** Connection-cycle guard; reads fresh edge state on every call. */ wouldCreateConnectionCycle: (source: string, target: string) => boolean + /** Deprecation badge — editor-only. When set, an amber "deprecated" badge shows; + * clicking (gated on `canFixDeprecation`) invokes `onFixDeprecation`. */ + deprecationTooltip?: string + canFixDeprecation?: boolean + onFixDeprecation?: () => void + /** Child-workflow deploy badge state — editor-only; omit in read-only contexts. */ isWorkflowSelector?: boolean childWorkflowId?: string @@ -132,6 +138,9 @@ export function WorkflowBlockView({ routerRows, routerContextValue, wouldCreateConnectionCycle, + deprecationTooltip, + canFixDeprecation, + onFixDeprecation, isWorkflowSelector, childWorkflowId, childIsDeployed, @@ -222,6 +231,28 @@ export function WorkflowBlockView({ />
+ {deprecationTooltip && ( + + + { + e.stopPropagation() + if (canFixDeprecation) onFixDeprecation?.() + }} + > + deprecated + + + + + {canFixDeprecation ? deprecationTooltip : 'Edit access required to fix'} + + + + )} {isWorkflowSelector && childWorkflowId && typeof childIsDeployed === 'boolean' && From 75951cf1a0b4c0d7fd42c0508094b132922c0111 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Mon, 20 Jul 2026 16:55:59 -0700 Subject: [PATCH 2/3] improvement(blocks): simplify deprecation derivation, drop unused getModelReplacement --- .../workflow-block/workflow-block.tsx | 89 +++++++++++-------- apps/sim/providers/models.test.ts | 15 ---- apps/sim/providers/models.ts | 18 +--- 3 files changed, 57 insertions(+), 65 deletions(-) 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 937746ec097..f9558e21f9e 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 @@ -49,7 +49,11 @@ import { useBlockVisual } from '@/app/workspace/[workspaceId]/w/[workflowId]/hoo import { useBlockDimensions } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-dimensions' import { useCustomBlockOverlayVersion } from '@/blocks/custom/client-overlay' import { getBlock } from '@/blocks/registry' -import { SELECTOR_TYPES_HYDRATION_REQUIRED, type SubBlockConfig } from '@/blocks/types' +import { + type BlockConfig, + SELECTOR_TYPES_HYDRATION_REQUIRED, + type SubBlockConfig, +} from '@/blocks/types' import { getDependsOnFields } from '@/blocks/utils' import { useKnowledgeBase } from '@/hooks/kb/use-knowledge' import { useCustomTools } from '@/hooks/queries/custom-tools' @@ -62,7 +66,7 @@ import { useTablesList } from '@/hooks/queries/tables' import { useWorkflowMap } from '@/hooks/queries/workflows' import { useReactiveConditions } from '@/hooks/use-reactive-conditions' import { useSelectorDisplayName } from '@/hooks/use-selector-display-name' -import { getModelReplacement, isModelDeprecated } from '@/providers/models' +import { isModelDeprecated } from '@/providers/models' import { useVariablesStore } from '@/stores/variables/store' import { useSubBlockStore } from '@/stores/workflows/subblock/store' import { useWorkflowStore } from '@/stores/workflows/workflow/store' @@ -77,6 +81,48 @@ const EMPTY_SUBBLOCK_VALUES = {} as Record /** Stable empty map for rows that never resolve MCP tool names */ const EMPTY_MCP_TOOL_NAMES: ReadonlyMap = new Map() +interface BlockDeprecation { + kind: 'block' | 'model' + tooltip: string + prompt: string +} + +/** + * Deprecation state for a placed block: the block type itself (via + * `config.deprecated.replacedBy`) or its selected model. `null` when neither + * applies or in diff mode. Drives the canvas badge + click-to-fix prompt. + */ +function getBlockDeprecation( + config: BlockConfig, + name: string, + model: unknown, + isDiffMode: boolean +): BlockDeprecation | null { + if (isDiffMode) return null + + const replacedBy = config.deprecated?.replacedBy + if (replacedBy) { + const target = getBlock(replacedBy) + if (!target) return null + const hasModel = config.subBlocks?.some((sub) => sub.id === 'model') + return { + kind: 'block', + tooltip: 'This block is deprecated. Click to upgrade', + prompt: `The "${name}" block is deprecated. Migrate it to the current ${target.name} block: change the block type, then set the new block's required inputs as a separate edit (inputs are validated against the old type when sent in the same edit), or delete it and re-add ${target.name} and rewire the connections.${hasModel ? ' Also pick a current, non-deprecated model.' : ''}`, + } + } + + if (typeof model === 'string' && isModelDeprecated(model)) { + return { + kind: 'model', + tooltip: `${model} is deprecated. Click to upgrade`, + prompt: `The "${name}" block uses the deprecated model "${model}". Switch it to the latest equivalent model.`, + } + } + + return null +} + interface SubBlockRowProps { title: string value?: string @@ -487,41 +533,14 @@ export const WorkflowBlock = memo(function WorkflowBlock({ const posthog = usePostHog() - const deprecation = useMemo(() => { - if (currentWorkflow.isDiffMode) return null - - const replacedBy = config.deprecated?.replacedBy - if (replacedBy) { - const target = getBlock(replacedBy) - if (!target) return null - const hasModel = config.subBlocks?.some((sub) => sub.id === 'model') - return { - kind: 'block' as const, - tooltip: 'This block is deprecated. Click to upgrade', - prompt: `The "${name}" block is deprecated. Migrate it to the current ${target.name} block: change the block type, then set the new block's required inputs as a separate edit (inputs are validated against the old type when sent in the same edit), or delete it and re-add ${target.name} and rewire the connections.${hasModel ? ' Also pick a current, non-deprecated model.' : ''}`, - } - } - - const model = blockSubBlockValues.model - if (typeof model === 'string' && isModelDeprecated(model)) { - if (!getModelReplacement(model)) return null - return { - kind: 'model' as const, - tooltip: `${model} is deprecated. Click to upgrade`, - prompt: `The "${name}" block uses the deprecated model "${model}". Switch it to the latest equivalent model.`, - } - } - - return null - }, [ - config.deprecated, - config.subBlocks, + const deprecation = getBlockDeprecation( + config, name, blockSubBlockValues.model, - currentWorkflow.isDiffMode, - ]) + currentWorkflow.isDiffMode + ) - const onFixDeprecation = useCallback(() => { + const onFixDeprecation = () => { if (!deprecation) return captureEvent(posthog, 'deprecated_block_fix_clicked', { block_type: type, @@ -531,7 +550,7 @@ export const WorkflowBlock = memo(function WorkflowBlock({ sendMothershipMessage(deprecation.prompt, [ { kind: 'workflow_block', workflowId: currentWorkflowId, blockId: id, label: name }, ]) - }, [deprecation, posthog, type, currentWorkflowId, id, name]) + } const canonicalIndex = useMemo(() => buildCanonicalIndex(config.subBlocks), [config.subBlocks]) const canonicalModeOverrides = currentStoreBlock?.data?.canonicalModes diff --git a/apps/sim/providers/models.test.ts b/apps/sim/providers/models.test.ts index c7028a3f4ef..1df24dad98b 100644 --- a/apps/sim/providers/models.test.ts +++ b/apps/sim/providers/models.test.ts @@ -5,7 +5,6 @@ import { describe, expect, it } from 'vitest' import { getBaseModelProviders, getHostedModels, - getModelReplacement, isModelDeprecated, orderModelIdsByReleaseDate, PROVIDER_DEFINITIONS, @@ -339,17 +338,3 @@ describe('isModelDeprecated', () => { expect(isModelDeprecated('openrouter/some/model')).toBe(false) }) }) - -describe('getModelReplacement', () => { - it('returns a non-deprecated replacement for a deprecated model', () => { - const id = firstDeprecatedModelId()! - const replacement = getModelReplacement(id) - if (replacement !== null) { - expect(isModelDeprecated(replacement)).toBe(false) - } - }) - - it('returns null for a non-deprecated model', () => { - expect(getModelReplacement('claude-sonnet-5')).toBeNull() - }) -}) diff --git a/apps/sim/providers/models.ts b/apps/sim/providers/models.ts index 5ccda8e6043..d307316520d 100644 --- a/apps/sim/providers/models.ts +++ b/apps/sim/providers/models.ts @@ -3850,11 +3850,11 @@ export function isKnownModelId(modelId: string): boolean { return false } -const DEPRECATED_STATIC_MODELS = new Map() +const DEPRECATED_STATIC_MODEL_IDS = new Set() for (const [providerId, provider] of Object.entries(PROVIDER_DEFINITIONS)) { if ((DYNAMIC_MODEL_PROVIDERS as readonly string[]).includes(providerId)) continue for (const model of provider.models) { - if (model.deprecated) DEPRECATED_STATIC_MODELS.set(model.id.toLowerCase(), { providerId }) + if (model.deprecated) DEPRECATED_STATIC_MODEL_IDS.add(model.id.toLowerCase()) } } @@ -3863,19 +3863,7 @@ for (const [providerId, provider] of Object.entries(PROVIDER_DEFINITIONS)) { * and unknown ids are never deprecated (they carry no static catalog entry). */ export function isModelDeprecated(modelId: string | undefined | null): boolean { - return !!modelId && DEPRECATED_STATIC_MODELS.has(modelId.toLowerCase()) -} - -/** - * The current model a deprecated model should migrate to — its provider's - * default, when that default is itself non-deprecated. `null` when no clean - * target exists, in which case the model is not surfaced as fixable. - */ -export function getModelReplacement(modelId: string): string | null { - const entry = DEPRECATED_STATIC_MODELS.get(modelId.toLowerCase()) - if (!entry) return null - const def = PROVIDER_DEFINITIONS[entry.providerId]?.defaultModel - return def && !isModelDeprecated(def) ? def : null + return !!modelId && DEPRECATED_STATIC_MODEL_IDS.has(modelId.toLowerCase()) } function getRecommendedModels(): string[] { From 24c425f1afd22337e3b167883312f889b699a507 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Mon, 20 Jul 2026 16:59:11 -0700 Subject: [PATCH 3/3] fix(blocks): make deprecation badge keyboard-accessible --- .../src/workflow-block/workflow-block-view.tsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/workflow-renderer/src/workflow-block/workflow-block-view.tsx b/packages/workflow-renderer/src/workflow-block/workflow-block-view.tsx index ff4b75d649e..536cc23b487 100644 --- a/packages/workflow-renderer/src/workflow-block/workflow-block-view.tsx +++ b/packages/workflow-renderer/src/workflow-block/workflow-block-view.tsx @@ -238,10 +238,20 @@ export function WorkflowBlockView({ variant='amber' className={canFixDeprecation ? 'cursor-pointer' : 'cursor-not-allowed'} dot + role={canFixDeprecation ? 'button' : undefined} + tabIndex={canFixDeprecation ? 0 : undefined} onClick={(e) => { e.stopPropagation() if (canFixDeprecation) onFixDeprecation?.() }} + onKeyDown={ + canFixDeprecation + ? (e) => { + e.stopPropagation() + handleKeyboardActivation(e, () => onFixDeprecation?.()) + } + : undefined + } > deprecated