From 0567392f271f01ffc1650fe70ec071157ef98e04 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Mon, 20 Jul 2026 18:45:15 -0700 Subject: [PATCH 1/3] improvement(blocks): two-tier sunset schema (legacy amber / deprecated red) --- .../workflow-block/workflow-block.tsx | 69 ++++++++++++------- apps/sim/blocks/blocks/api_trigger.ts | 2 +- apps/sim/blocks/blocks/chat_trigger.ts | 2 +- apps/sim/blocks/blocks/confluence.ts | 4 +- apps/sim/blocks/blocks/cursor.ts | 4 +- apps/sim/blocks/blocks/extend.ts | 4 +- apps/sim/blocks/blocks/file.ts | 10 +-- apps/sim/blocks/blocks/fireflies.ts | 4 +- apps/sim/blocks/blocks/github.ts | 4 +- apps/sim/blocks/blocks/gmail.ts | 4 +- apps/sim/blocks/blocks/google_calendar.ts | 4 +- apps/sim/blocks/blocks/google_sheets.ts | 2 +- apps/sim/blocks/blocks/google_slides.ts | 4 +- apps/sim/blocks/blocks/grain.ts | 4 +- apps/sim/blocks/blocks/image_generator.ts | 2 +- apps/sim/blocks/blocks/input_trigger.ts | 2 +- apps/sim/blocks/blocks/intercom.ts | 4 +- apps/sim/blocks/blocks/kalshi.ts | 4 +- apps/sim/blocks/blocks/linear.ts | 4 +- apps/sim/blocks/blocks/logs.ts | 2 +- apps/sim/blocks/blocks/manual_trigger.ts | 2 +- apps/sim/blocks/blocks/microsoft_excel.ts | 2 +- apps/sim/blocks/blocks/mistral_parse.ts | 6 +- apps/sim/blocks/blocks/notion.ts | 2 +- apps/sim/blocks/blocks/pulse.ts | 4 +- apps/sim/blocks/blocks/reducto.ts | 4 +- apps/sim/blocks/blocks/router.ts | 2 +- apps/sim/blocks/blocks/sharepoint.ts | 4 +- apps/sim/blocks/blocks/starter.ts | 2 +- apps/sim/blocks/blocks/stt.ts | 4 +- apps/sim/blocks/blocks/textract.ts | 4 +- apps/sim/blocks/blocks/video_generator.ts | 6 +- apps/sim/blocks/blocks/vision.ts | 2 - apps/sim/blocks/blocks/workflow.ts | 2 +- apps/sim/blocks/types.ts | 12 ++-- apps/sim/scripts/check-block-registry.ts | 24 +++---- .../workflow-block/workflow-block-view.tsx | 38 +++++----- 37 files changed, 139 insertions(+), 120 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 f9558e21f9e..b194dc646f5 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 @@ -81,39 +81,60 @@ 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 { +interface BlockSunset { + status: 'legacy' | 'deprecated' kind: 'block' | 'model' tooltip: string prompt: string } +/** Instruction for the agent to migrate a block instance to its successor. */ +function migrationPrompt(name: string, target: BlockConfig): string { + return `Migrate the "${name}" block 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.` +} + /** - * 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. + * Sunset state for a placed block: the block type itself (via `config.sunset`) + * or its selected model. `legacy` (amber) is superseded-but-supported and needs + * a resolvable successor; `deprecated` (red) is no longer supported and badges + * with or without one. `null` when neither applies or in diff mode. */ -function getBlockDeprecation( +function getBlockSunset( config: BlockConfig, name: string, model: unknown, isDiffMode: boolean -): BlockDeprecation | null { +): BlockSunset | 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') + const sunset = config.sunset + if (sunset) { + const target = sunset.replacedBy ? getBlock(sunset.replacedBy) : undefined + + if (sunset.status === 'legacy') { + if (!target) return null + const hasModel = config.subBlocks?.some((sub) => sub.id === 'model') + return { + status: 'legacy', + kind: 'block', + tooltip: 'This block is legacy. Click to upgrade', + prompt: `The "${name}" block is legacy. ${migrationPrompt(name, target)}${hasModel ? ' Also pick a current, non-deprecated model.' : ''}`, + } + } + return { + status: 'deprecated', 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.' : ''}`, + tooltip: 'This block is no longer supported. Click to replace', + prompt: target + ? `The "${name}" block is no longer supported. ${migrationPrompt(name, target)}` + : `The "${name}" block is no longer supported and has no direct successor. Replace it with current blocks that achieve the same result and rewire the connections.`, } } if (typeof model === 'string' && isModelDeprecated(model)) { return { + status: 'legacy', 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.`, @@ -533,21 +554,16 @@ export const WorkflowBlock = memo(function WorkflowBlock({ const posthog = usePostHog() - const deprecation = getBlockDeprecation( - config, - name, - blockSubBlockValues.model, - currentWorkflow.isDiffMode - ) + const sunset = getBlockSunset(config, name, blockSubBlockValues.model, currentWorkflow.isDiffMode) - const onFixDeprecation = () => { - if (!deprecation) return + const onFixSunset = () => { + if (!sunset) return captureEvent(posthog, 'deprecated_block_fix_clicked', { block_type: type, workflow_id: currentWorkflowId, - kind: deprecation.kind, + kind: sunset.kind, }) - sendMothershipMessage(deprecation.prompt, [ + sendMothershipMessage(sunset.prompt, [ { kind: 'workflow_block', workflowId: currentWorkflowId, blockId: id, label: name }, ]) } @@ -871,9 +887,10 @@ export const WorkflowBlock = memo(function WorkflowBlock({ deployChildWorkflow({ workflowId: childWorkflowId }) } }} - deprecationTooltip={deprecation?.tooltip} - canFixDeprecation={canEditWorkflow} - onFixDeprecation={onFixDeprecation} + sunsetStatus={sunset?.status} + sunsetTooltip={sunset?.tooltip} + canFixSunset={canEditWorkflow} + onFixSunset={onFixSunset} 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 dac480ad7d4..27ad2beef33 100644 --- a/apps/sim/blocks/blocks/api_trigger.ts +++ b/apps/sim/blocks/blocks/api_trigger.ts @@ -15,7 +15,7 @@ export const ApiTriggerBlock: BlockConfig = { `, category: 'triggers', hideFromToolbar: true, - deprecated: { replacedBy: 'start_trigger' }, + sunset: { status: 'legacy', 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 27294a135f6..f4379575407 100644 --- a/apps/sim/blocks/blocks/chat_trigger.ts +++ b/apps/sim/blocks/blocks/chat_trigger.ts @@ -16,7 +16,7 @@ export const ChatTriggerBlock: BlockConfig = { `, category: 'triggers', hideFromToolbar: true, - deprecated: { replacedBy: 'start_trigger' }, + sunset: { status: 'legacy', 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 edadf86d709..54d63a3b80e 100644 --- a/apps/sim/blocks/blocks/confluence.ts +++ b/apps/sim/blocks/blocks/confluence.ts @@ -12,7 +12,7 @@ export const ConfluenceBlock: BlockConfig = { name: 'Confluence (Legacy)', description: 'Interact with Confluence', hideFromToolbar: true, - deprecated: { replacedBy: 'confluence_v2' }, + sunset: { status: 'legacy', 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.', @@ -361,7 +361,7 @@ export const ConfluenceBlock: BlockConfig = { export const ConfluenceV2Block: BlockConfig = { ...ConfluenceBlock, - deprecated: undefined, + sunset: 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 f7031840589..4c250ad7b87 100644 --- a/apps/sim/blocks/blocks/cursor.ts +++ b/apps/sim/blocks/blocks/cursor.ts @@ -17,7 +17,7 @@ export const CursorBlock: BlockConfig = { icon: CursorIcon, authMode: AuthMode.ApiKey, hideFromToolbar: true, - deprecated: { replacedBy: 'cursor_v2' }, + sunset: { status: 'legacy', replacedBy: 'cursor_v2' }, subBlocks: [ { id: 'operation', @@ -224,7 +224,7 @@ export const CursorBlock: BlockConfig = { export const CursorV2Block: BlockConfig = { ...CursorBlock, - deprecated: undefined, + sunset: 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 f06a853df3f..167cada4f98 100644 --- a/apps/sim/blocks/blocks/extend.ts +++ b/apps/sim/blocks/blocks/extend.ts @@ -14,7 +14,7 @@ export const ExtendBlock: BlockConfig = { name: 'Extend', description: 'Parse and extract content from documents', hideFromToolbar: true, - deprecated: { replacedBy: 'extend_v2' }, + sunset: { status: 'legacy', 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.', @@ -166,7 +166,7 @@ const extendV2SubBlocks = (ExtendBlock.subBlocks || []).flatMap((subBlock) => { export const ExtendV2Block: BlockConfig = { ...ExtendBlock, - deprecated: undefined, + sunset: 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 52a92261267..409eb9824ac 100644 --- a/apps/sim/blocks/blocks/file.ts +++ b/apps/sim/blocks/blocks/file.ts @@ -78,7 +78,7 @@ export const FileBlock: BlockConfig = { bgColor: '#40916C', icon: DocumentIcon, hideFromToolbar: true, - deprecated: { replacedBy: 'file_v5' }, + sunset: { status: 'legacy', replacedBy: 'file_v5' }, subBlocks: [ { id: 'inputMethod', @@ -186,7 +186,7 @@ export const FileV2Block: BlockConfig = { name: 'File (Legacy)', description: 'Read and parse multiple files', hideFromToolbar: true, - deprecated: { replacedBy: 'file_v5' }, + sunset: { status: 'legacy', replacedBy: 'file_v5' }, subBlocks: [ { id: 'file', @@ -280,7 +280,7 @@ export const FileV3Block: BlockConfig = { bgColor: '#40916C', icon: DocumentIcon, hideFromToolbar: true, - deprecated: { replacedBy: 'file_v5' }, + sunset: { status: 'legacy', replacedBy: 'file_v5' }, subBlocks: [ { id: 'operation', @@ -571,7 +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' }, + sunset: { status: 'legacy', 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. @@ -824,7 +824,7 @@ export const FileV4Block: BlockConfig = { export const FileV5Block: BlockConfig = { ...FileV4Block, - deprecated: undefined, + sunset: undefined, type: 'file_v5', name: 'File', description: diff --git a/apps/sim/blocks/blocks/fireflies.ts b/apps/sim/blocks/blocks/fireflies.ts index 9d4adfb587a..b446a0e6716 100644 --- a/apps/sim/blocks/blocks/fireflies.ts +++ b/apps/sim/blocks/blocks/fireflies.ts @@ -11,7 +11,7 @@ export const FirefliesBlock: BlockConfig = { name: 'Fireflies (Legacy)', description: 'Interact with Fireflies.ai meeting transcripts and recordings', hideFromToolbar: true, - deprecated: { replacedBy: 'fireflies_v2' }, + sunset: { status: 'legacy', replacedBy: 'fireflies_v2' }, authMode: AuthMode.ApiKey, triggerAllowed: true, longDescription: @@ -647,7 +647,7 @@ const firefliesV2Inputs = FirefliesBlock.inputs export const FirefliesV2Block: BlockConfig = { ...FirefliesBlock, - deprecated: undefined, + sunset: 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 ef574b47003..e035757ff08 100644 --- a/apps/sim/blocks/blocks/github.ts +++ b/apps/sim/blocks/blocks/github.ts @@ -20,7 +20,7 @@ export const GitHubBlock: BlockConfig = { icon: GithubIcon, triggerAllowed: true, hideFromToolbar: true, - deprecated: { replacedBy: 'github_v2' }, + sunset: { status: 'legacy', replacedBy: 'github_v2' }, subBlocks: [ { id: 'operation', @@ -2103,7 +2103,7 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, export const GitHubV2Block: BlockConfig = { ...GitHubBlock, - deprecated: undefined, + sunset: 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 2abdfe49339..af65d44ffd5 100644 --- a/apps/sim/blocks/blocks/gmail.ts +++ b/apps/sim/blocks/blocks/gmail.ts @@ -57,7 +57,7 @@ export const GmailBlock: BlockConfig = { bgColor: '#FFFFFF', icon: GmailIcon, hideFromToolbar: true, - deprecated: { replacedBy: 'gmail_v2' }, + sunset: { status: 'legacy', replacedBy: 'gmail_v2' }, triggerAllowed: true, subBlocks: [ // Operation selector @@ -576,7 +576,7 @@ Return ONLY the search query - no explanations, no extra text.`, export const GmailV2Block: BlockConfig = { ...GmailBlock, - deprecated: undefined, + sunset: 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 e330d8d1ec2..4c626ded998 100644 --- a/apps/sim/blocks/blocks/google_calendar.ts +++ b/apps/sim/blocks/blocks/google_calendar.ts @@ -19,7 +19,7 @@ export const GoogleCalendarBlock: BlockConfig = { bgColor: '#FFFFFF', icon: GoogleCalendarIcon, hideFromToolbar: true, - deprecated: { replacedBy: 'google_calendar_v2' }, + sunset: { status: 'legacy', replacedBy: 'google_calendar_v2' }, subBlocks: [ { id: 'operation', @@ -926,7 +926,7 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, export const GoogleCalendarV2Block: BlockConfig = { ...GoogleCalendarBlock, - deprecated: undefined, + sunset: 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 4ef9ff8b7ad..adaa886297d 100644 --- a/apps/sim/blocks/blocks/google_sheets.ts +++ b/apps/sim/blocks/blocks/google_sheets.ts @@ -13,7 +13,7 @@ export const GoogleSheetsBlock: BlockConfig = { description: 'Read, write, and update data', authMode: AuthMode.OAuth, hideFromToolbar: true, - deprecated: { replacedBy: 'google_sheets_v2' }, + sunset: { status: 'legacy', 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 26a8a100723..19184b92fe6 100644 --- a/apps/sim/blocks/blocks/google_slides.ts +++ b/apps/sim/blocks/blocks/google_slides.ts @@ -11,7 +11,7 @@ export const GoogleSlidesBlock: BlockConfig = { name: 'Google Slides (Legacy)', description: 'Read, write, and create presentations', hideFromToolbar: true, - deprecated: { replacedBy: 'google_slides_v2' }, + sunset: { status: 'legacy', 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.', @@ -3455,7 +3455,7 @@ const googleSlidesV2Inputs = GoogleSlidesBlock.inputs export const GoogleSlidesV2Block: BlockConfig = { ...GoogleSlidesBlock, - deprecated: undefined, + sunset: 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 14bf0eb3bd7..07ed97f7f25 100644 --- a/apps/sim/blocks/blocks/grain.ts +++ b/apps/sim/blocks/blocks/grain.ts @@ -27,7 +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' }, + sunset: { status: 'legacy', 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', @@ -502,7 +502,7 @@ Return ONLY the search term - no explanations, no quotes, no extra text.`, */ export const GrainV2Block: BlockConfig = { ...GrainBlock, - deprecated: undefined, + sunset: 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 f47b14b56f7..0364972a9d7 100644 --- a/apps/sim/blocks/blocks/image_generator.ts +++ b/apps/sim/blocks/blocks/image_generator.ts @@ -57,7 +57,7 @@ export const ImageGeneratorBlock: BlockConfig = { name: 'Image Generator', description: 'Generate images', hideFromToolbar: true, - deprecated: { replacedBy: 'image_generator_v2' }, + sunset: { status: 'legacy', 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 beab6a48f12..c2891e5fa58 100644 --- a/apps/sim/blocks/blocks/input_trigger.ts +++ b/apps/sim/blocks/blocks/input_trigger.ts @@ -19,7 +19,7 @@ export const InputTriggerBlock: BlockConfig = { `, category: 'triggers', hideFromToolbar: true, - deprecated: { replacedBy: 'start_trigger' }, + sunset: { status: 'legacy', 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 f09c0ed75aa..78001b7c0a6 100644 --- a/apps/sim/blocks/blocks/intercom.ts +++ b/apps/sim/blocks/blocks/intercom.ts @@ -8,7 +8,7 @@ export const IntercomBlock: BlockConfig = { type: 'intercom', name: 'Intercom (Legacy)', hideFromToolbar: true, - deprecated: { replacedBy: 'intercom_v2' }, + sunset: { status: 'legacy', 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.', @@ -1405,7 +1405,7 @@ Return ONLY the numeric timestamp.`, export const IntercomV2Block: BlockConfig = { ...IntercomBlock, - deprecated: undefined, + sunset: 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 31ae5cae1b2..799b9ad64bb 100644 --- a/apps/sim/blocks/blocks/kalshi.ts +++ b/apps/sim/blocks/blocks/kalshi.ts @@ -14,7 +14,7 @@ export const KalshiBlock: BlockConfig = { category: 'tools', integrationType: IntegrationType.Analytics, hideFromToolbar: true, - deprecated: { replacedBy: 'kalshi_v2' }, + sunset: { status: 'legacy', replacedBy: 'kalshi_v2' }, bgColor: '#09C285', iconColor: '#09C285', icon: KalshiIcon, @@ -763,7 +763,7 @@ Return ONLY the numeric timestamp (seconds since Unix epoch) - no explanations, export const KalshiV2Block: BlockConfig = { ...KalshiBlock, - deprecated: undefined, + sunset: 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 7c89a2b7d1b..788d433256d 100644 --- a/apps/sim/blocks/blocks/linear.ts +++ b/apps/sim/blocks/blocks/linear.ts @@ -11,7 +11,7 @@ export const LinearBlock: BlockConfig = { name: 'Linear (Legacy)', description: 'Interact with Linear issues, projects, and more', hideFromToolbar: true, - deprecated: { replacedBy: 'linear_v2' }, + sunset: { status: 'legacy', replacedBy: 'linear_v2' }, authMode: AuthMode.OAuth, triggerAllowed: true, longDescription: @@ -2562,7 +2562,7 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n */ export const LinearV2Block: BlockConfig = { ...LinearBlock, - deprecated: undefined, + sunset: 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 0c5c88d8a6f..b42464f55e0 100644 --- a/apps/sim/blocks/blocks/logs.ts +++ b/apps/sim/blocks/blocks/logs.ts @@ -6,7 +6,7 @@ export const LogsBlock: BlockConfig = { type: 'logs', name: 'Logs', hideFromToolbar: true, - deprecated: { replacedBy: 'logs_v2' }, + sunset: { status: 'legacy', 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 1ceb98b559b..6ba153886f6 100644 --- a/apps/sim/blocks/blocks/manual_trigger.ts +++ b/apps/sim/blocks/blocks/manual_trigger.ts @@ -18,7 +18,7 @@ export const ManualTriggerBlock: BlockConfig = { `, category: 'triggers', hideFromToolbar: true, - deprecated: { replacedBy: 'start_trigger' }, + sunset: { status: 'legacy', 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 2143c016b86..6c624157cf3 100644 --- a/apps/sim/blocks/blocks/microsoft_excel.ts +++ b/apps/sim/blocks/blocks/microsoft_excel.ts @@ -60,7 +60,7 @@ export const MicrosoftExcelBlock: BlockConfig = { description: 'Read, write, and update data', authMode: AuthMode.OAuth, hideFromToolbar: true, - deprecated: { replacedBy: 'microsoft_excel_v2' }, + sunset: { status: 'legacy', 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 4025be7a6e6..5530d8cbfd5 100644 --- a/apps/sim/blocks/blocks/mistral_parse.ts +++ b/apps/sim/blocks/blocks/mistral_parse.ts @@ -15,7 +15,7 @@ export const MistralParseBlock: BlockConfig = { name: 'Mistral Parser (Legacy)', description: 'Extract text from PDF documents', hideFromToolbar: true, - deprecated: { replacedBy: 'mistral_parse_v3' }, + sunset: { status: 'legacy', 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', @@ -162,7 +162,7 @@ export const MistralParseV2Block: BlockConfig = { name: 'Mistral Parser', description: 'Extract text from PDF documents', hideFromToolbar: true, - deprecated: { replacedBy: 'mistral_parse_v3' }, + sunset: { status: 'legacy', replacedBy: 'mistral_parse_v3' }, subBlocks: [ { id: 'fileUpload', @@ -289,7 +289,7 @@ export const MistralParseV2Block: BlockConfig = { */ export const MistralParseV3Block: BlockConfig = { ...MistralParseBlock, - deprecated: undefined, + sunset: 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 668a89901a3..1de10150f71 100644 --- a/apps/sim/blocks/blocks/notion.ts +++ b/apps/sim/blocks/blocks/notion.ts @@ -12,7 +12,7 @@ export const NotionBlock: BlockConfig = { type: 'notion', name: 'Notion (Legacy)', hideFromToolbar: true, - deprecated: { replacedBy: 'notion_v2' }, + sunset: { status: 'legacy', 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 7bcaa364b88..18a0f6b7b06 100644 --- a/apps/sim/blocks/blocks/pulse.ts +++ b/apps/sim/blocks/blocks/pulse.ts @@ -14,7 +14,7 @@ export const PulseBlock: BlockConfig = { name: 'Pulse', description: 'Extract text from documents using Pulse OCR', hideFromToolbar: true, - deprecated: { replacedBy: 'pulse_v2' }, + sunset: { status: 'legacy', 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.', @@ -163,7 +163,7 @@ const pulseV2SubBlocks = (PulseBlock.subBlocks || []).flatMap((subBlock) => { export const PulseV2Block: BlockConfig = { ...PulseBlock, - deprecated: undefined, + sunset: 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 02bf3ff02be..edfe6ca492e 100644 --- a/apps/sim/blocks/blocks/reducto.ts +++ b/apps/sim/blocks/blocks/reducto.ts @@ -15,7 +15,7 @@ export const ReductoBlock: BlockConfig = { name: 'Reducto', description: 'Extract text from PDF documents', hideFromToolbar: true, - deprecated: { replacedBy: 'reducto_v2' }, + sunset: { status: 'legacy', 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', @@ -169,7 +169,7 @@ const reductoV2SubBlocks = (ReductoBlock.subBlocks || []).flatMap((subBlock) => export const ReductoV2Block: BlockConfig = { ...ReductoBlock, - deprecated: undefined, + sunset: 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 76d156e776c..beebedf27ef 100644 --- a/apps/sim/blocks/blocks/router.ts +++ b/apps/sim/blocks/blocks/router.ts @@ -157,7 +157,7 @@ export const RouterBlock: BlockConfig = { bgColor: '#28C43F', icon: ConnectIcon, hideFromToolbar: true, // Hide legacy version from toolbar - deprecated: { replacedBy: 'router_v2' }, + sunset: { status: 'legacy', replacedBy: 'router_v2' }, subBlocks: [ { id: 'prompt', diff --git a/apps/sim/blocks/blocks/sharepoint.ts b/apps/sim/blocks/blocks/sharepoint.ts index b19e5c84c89..4f64573d690 100644 --- a/apps/sim/blocks/blocks/sharepoint.ts +++ b/apps/sim/blocks/blocks/sharepoint.ts @@ -15,7 +15,7 @@ export const SharepointBlock: BlockConfig = { description: 'Work with pages and lists', authMode: AuthMode.OAuth, hideFromToolbar: true, - deprecated: { replacedBy: 'sharepoint_v2' }, + sunset: { status: 'legacy', 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', @@ -822,7 +822,7 @@ const SHAREPOINT_V2_PAGE_MUTATION_OPERATIONS = [ export const SharepointV2Block: BlockConfig = { ...SharepointBlock, - deprecated: undefined, + sunset: 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 00196da3f24..afae4a12f94 100644 --- a/apps/sim/blocks/blocks/starter.ts +++ b/apps/sim/blocks/blocks/starter.ts @@ -10,7 +10,7 @@ export const StarterBlock: BlockConfig = { bgColor: '#2FB3FF', icon: StartIcon, hideFromToolbar: true, - deprecated: { replacedBy: 'start_trigger' }, + sunset: { status: 'legacy', replacedBy: 'start_trigger' }, subBlocks: [ // Main trigger selector { diff --git a/apps/sim/blocks/blocks/stt.ts b/apps/sim/blocks/blocks/stt.ts index 0d75264dc87..976abd1c2d5 100644 --- a/apps/sim/blocks/blocks/stt.ts +++ b/apps/sim/blocks/blocks/stt.ts @@ -8,7 +8,7 @@ export const SttBlock: BlockConfig = { name: 'Speech-to-Text', description: 'Convert speech to text using AI', hideFromToolbar: true, - deprecated: { replacedBy: 'stt_v2' }, + sunset: { status: 'legacy', 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.', @@ -362,7 +362,7 @@ const sttV2SubBlocks = (SttBlock.subBlocks || []).filter((subBlock) => subBlock. export const SttV2Block: BlockConfig = { ...SttBlock, - deprecated: undefined, + sunset: 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 d48bb3e830a..ff31b10ce43 100644 --- a/apps/sim/blocks/blocks/textract.ts +++ b/apps/sim/blocks/blocks/textract.ts @@ -14,7 +14,7 @@ export const TextractBlock: BlockConfig = { name: 'AWS Textract', description: 'Extract text, tables, and forms from documents', hideFromToolbar: true, - deprecated: { replacedBy: 'textract_v2' }, + sunset: { status: 'legacy', 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', @@ -239,7 +239,7 @@ function requireAwsCredentials(params: Record) { export const TextractV2Block: BlockConfig = { ...TextractBlock, - deprecated: undefined, + sunset: 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 cbd933bd8ba..3d5835ef0c7 100644 --- a/apps/sim/blocks/blocks/video_generator.ts +++ b/apps/sim/blocks/blocks/video_generator.ts @@ -75,7 +75,7 @@ export const VideoGeneratorBlock: BlockConfig = { name: 'Video Generator (Legacy)', description: 'Generate videos from text using AI', hideFromToolbar: true, - deprecated: { replacedBy: 'video_generator_v3' }, + sunset: { status: 'legacy', 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.', @@ -893,7 +893,7 @@ export const VideoGeneratorV2Block: BlockConfig = { type: 'video_generator_v2', name: 'Video Generator', hideFromToolbar: true, - deprecated: { replacedBy: 'video_generator_v3' }, + sunset: { status: 'legacy', replacedBy: 'video_generator_v3' }, subBlocks: [ { id: 'provider', @@ -1651,7 +1651,7 @@ export const VideoGeneratorV2Block: BlockConfig = { export const VideoGeneratorV3Block: BlockConfig = { ...VideoGeneratorV2Block, - deprecated: undefined, + sunset: 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 d7cb6d0ffb1..16e3b3aaa39 100644 --- a/apps/sim/blocks/blocks/vision.ts +++ b/apps/sim/blocks/blocks/vision.ts @@ -26,7 +26,6 @@ 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', @@ -110,7 +109,6 @@ 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 1aa6cfba87a..f54485345ba 100644 --- a/apps/sim/blocks/blocks/workflow.ts +++ b/apps/sim/blocks/blocks/workflow.ts @@ -64,5 +64,5 @@ export const WorkflowBlock: BlockConfig = { }, }, hideFromToolbar: true, - deprecated: { replacedBy: 'workflow_input' }, + sunset: { status: 'legacy', replacedBy: 'workflow_input' }, } diff --git a/apps/sim/blocks/types.ts b/apps/sim/blocks/types.ts index ac202046a38..1a89f490ab8 100644 --- a/apps/sim/blocks/types.ts +++ b/apps/sim/blocks/types.ts @@ -497,13 +497,15 @@ export interface BlockConfig { */ 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 + * Post-GA lifecycle state. `legacy` — superseded but still supported (amber + * badge, click-to-upgrade); `deprecated` — no longer supported, slated for + * removal (red badge). Placed instances keep executing and rendering in both + * states. `replacedBy` is the block `type` to 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?: { + sunset?: { + status: 'legacy' | 'deprecated' replacedBy?: string } triggers?: { diff --git a/apps/sim/scripts/check-block-registry.ts b/apps/sim/scripts/check-block-registry.ts index 1f0a7a980b7..72d9beeaccc 100644 --- a/apps/sim/scripts/check-block-registry.ts +++ b/apps/sim/scripts/check-block-registry.ts @@ -280,23 +280,23 @@ function checkIntegrationMetaCoverage(): CheckResult { return { kind: 'fail', errors } } -function checkDeprecationReplacedBy(): CheckResult { +function checkSunsetReplacedBy(): CheckResult { const errors: string[] = [] for (const block of getAllBlocks()) { - const replacedBy = block.deprecated?.replacedBy + const replacedBy = block.sunset?.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.` + `Block "${block.type}" is sunset with replacedBy: '${replacedBy}', but no such block exists.` ) continue } - if (target.deprecated) { + if (target.sunset) { errors.push( - `Block "${block.type}" points replacedBy: '${replacedBy}', but that block is itself deprecated.` + `Block "${block.type}" points replacedBy: '${replacedBy}', but that block is itself sunset.` ) } if (target.preview) { @@ -307,7 +307,7 @@ function checkDeprecationReplacedBy(): CheckResult { } if (errors.length === 0) { - return { kind: 'pass', message: 'Deprecation replacedBy check passed' } + return { kind: 'pass', message: 'Sunset replacedBy check passed' } } return { kind: 'fail', errors } } @@ -332,7 +332,7 @@ function reportResult(label: string, failureHeader: string, result: CheckResult) const stabilityResult = checkSubblockIdStability() const canonicalResult = checkCanonicalIdContract() const metaCoverageResult = checkIntegrationMetaCoverage() -const deprecationResult = checkDeprecationReplacedBy() +const sunsetResult = checkSunsetReplacedBy() const stabilityOk = reportResult( 'Subblock ID stability check', @@ -352,10 +352,10 @@ const metaCoverageOk = reportResult( metaCoverageResult ) -const deprecationOk = reportResult( - 'Deprecation replacedBy check', - 'A deprecated block must point replacedBy at a real, GA, non-deprecated successor.', - deprecationResult +const sunsetOk = reportResult( + 'Sunset replacedBy check', + 'A sunset block must point replacedBy at a real, GA, non-sunset successor.', + sunsetResult ) -process.exit(stabilityOk && canonicalOk && metaCoverageOk && deprecationOk ? 0 : 1) +process.exit(stabilityOk && canonicalOk && metaCoverageOk && sunsetOk ? 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 536cc23b487..2ee141b10b2 100644 --- a/packages/workflow-renderer/src/workflow-block/workflow-block-view.tsx +++ b/packages/workflow-renderer/src/workflow-block/workflow-block-view.tsx @@ -70,11 +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 + /** Sunset badge — editor-only. `legacy` shows an amber "legacy" badge, `deprecated` + * a red "deprecated" badge; clicking (gated on `canFixSunset`) invokes `onFixSunset`. */ + sunsetStatus?: 'legacy' | 'deprecated' + sunsetTooltip?: string + canFixSunset?: boolean + onFixSunset?: () => void /** Child-workflow deploy badge state — editor-only; omit in read-only contexts. */ isWorkflowSelector?: boolean @@ -138,9 +139,10 @@ export function WorkflowBlockView({ routerRows, routerContextValue, wouldCreateConnectionCycle, - deprecationTooltip, - canFixDeprecation, - onFixDeprecation, + sunsetStatus, + sunsetTooltip, + canFixSunset, + onFixSunset, isWorkflowSelector, childWorkflowId, childIsDeployed, @@ -231,34 +233,34 @@ export function WorkflowBlockView({ />
- {deprecationTooltip && ( + {sunsetStatus && ( { e.stopPropagation() - if (canFixDeprecation) onFixDeprecation?.() + if (canFixSunset) onFixSunset?.() }} onKeyDown={ - canFixDeprecation + canFixSunset ? (e) => { e.stopPropagation() - handleKeyboardActivation(e, () => onFixDeprecation?.()) + handleKeyboardActivation(e, () => onFixSunset?.()) } : undefined } > - deprecated + {sunsetStatus} - {canFixDeprecation ? deprecationTooltip : 'Edit access required to fix'} + {canFixSunset ? sunsetTooltip : 'Edit access required to fix'} From bbe942c77e3ea42f7323de75aafc41577d7804d6 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Mon, 20 Jul 2026 18:51:24 -0700 Subject: [PATCH 2/3] fix(blocks): require replacedBy on legacy sunset blocks in registry check --- apps/sim/scripts/check-block-registry.ts | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/apps/sim/scripts/check-block-registry.ts b/apps/sim/scripts/check-block-registry.ts index 72d9beeaccc..3aeb8d43571 100644 --- a/apps/sim/scripts/check-block-registry.ts +++ b/apps/sim/scripts/check-block-registry.ts @@ -284,24 +284,35 @@ function checkSunsetReplacedBy(): CheckResult { const errors: string[] = [] for (const block of getAllBlocks()) { - const replacedBy = block.sunset?.replacedBy - if (!replacedBy) continue + const sunset = block.sunset + if (!sunset) continue + + if (!sunset.replacedBy) { + // `legacy` needs a successor to render its badge + upgrade action; `deprecated` + // (red) legitimately badges without one. + if (sunset.status === 'legacy') { + errors.push( + `Block "${block.type}" is sunset (legacy) but has no replacedBy — legacy blocks must name a successor or they render no badge.` + ) + } + continue + } - const target = getBlock(replacedBy) + const target = getBlock(sunset.replacedBy) if (!target) { errors.push( - `Block "${block.type}" is sunset with replacedBy: '${replacedBy}', but no such block exists.` + `Block "${block.type}" is sunset with replacedBy: '${sunset.replacedBy}', but no such block exists.` ) continue } if (target.sunset) { errors.push( - `Block "${block.type}" points replacedBy: '${replacedBy}', but that block is itself sunset.` + `Block "${block.type}" points replacedBy: '${sunset.replacedBy}', but that block is itself sunset.` ) } if (target.preview) { errors.push( - `Block "${block.type}" points replacedBy: '${replacedBy}', but that block is preview (not GA).` + `Block "${block.type}" points replacedBy: '${sunset.replacedBy}', but that block is preview (not GA).` ) } } From 0240e681f97ccc1ae311b974c97a717de5d540c2 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Mon, 20 Jul 2026 19:00:49 -0700 Subject: [PATCH 3/3] improvement(blocks): reword legacy badge tooltip --- .../w/[workflowId]/components/workflow-block/workflow-block.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 b194dc646f5..8d9a4fb85e0 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 @@ -117,7 +117,7 @@ function getBlockSunset( return { status: 'legacy', kind: 'block', - tooltip: 'This block is legacy. Click to upgrade', + tooltip: 'This is a legacy block. Click to upgrade', prompt: `The "${name}" block is legacy. ${migrationPrompt(name, target)}${hasModel ? ' Also pick a current, non-deprecated model.' : ''}`, } }