@@ -7,9 +7,10 @@ import {
77 workflowEdges ,
88 workflowSubflows ,
99} from '@sim/db'
10+ import { credential } from '@sim/db/schema'
1011import { createLogger } from '@sim/logger'
1112import type { InferInsertModel , InferSelectModel } from 'drizzle-orm'
12- import { and , desc , eq , sql } from 'drizzle-orm'
13+ import { and , desc , eq , inArray , sql } from 'drizzle-orm'
1314import type { Edge } from 'reactflow'
1415import { v4 as uuidv4 } from 'uuid'
1516import type { DbOrTx } from '@/lib/db/types'
@@ -99,8 +100,10 @@ export async function loadDeployedWorkflowState(workflowId: string): Promise<Dep
99100
100101 const state = active . state as WorkflowState & { variables ?: Record < string , unknown > }
101102
103+ const { blocks : migratedBlocks } = await migrateCredentialIds ( state . blocks || { } )
104+
102105 return {
103- blocks : state . blocks || { } ,
106+ blocks : migratedBlocks ,
104107 edges : state . edges || [ ] ,
105108 loops : state . loops || { } ,
106109 parallels : state . parallels || { } ,
@@ -185,6 +188,97 @@ export function migrateAgentBlocksToMessagesFormat(
185188 )
186189}
187190
191+ const CREDENTIAL_SUBBLOCK_IDS = new Set ( [ 'credential' , 'triggerCredentials' ] )
192+
193+ /**
194+ * Migrates legacy `account.id` values to `credential.id` in OAuth subblocks.
195+ * Collects all potential legacy IDs in a single batch query for efficiency.
196+ * Also migrates `tool.params.credential` in agent block tool arrays.
197+ */
198+ async function migrateCredentialIds (
199+ blocks : Record < string , BlockState >
200+ ) : Promise < { blocks : Record < string , BlockState > ; migrated : boolean } > {
201+ const potentialLegacyIds = new Set < string > ( )
202+
203+ for ( const block of Object . values ( blocks ) ) {
204+ for ( const [ subBlockId , subBlock ] of Object . entries ( block . subBlocks || { } ) ) {
205+ const value = ( subBlock as { value ?: unknown } ) . value
206+ if (
207+ CREDENTIAL_SUBBLOCK_IDS . has ( subBlockId ) &&
208+ typeof value === 'string' &&
209+ value &&
210+ ! value . startsWith ( 'cred_' )
211+ ) {
212+ potentialLegacyIds . add ( value )
213+ }
214+
215+ if ( subBlockId === 'tools' && Array . isArray ( value ) ) {
216+ for ( const tool of value ) {
217+ const credParam = tool ?. params ?. credential
218+ if ( typeof credParam === 'string' && credParam && ! credParam . startsWith ( 'cred_' ) ) {
219+ potentialLegacyIds . add ( credParam )
220+ }
221+ }
222+ }
223+ }
224+ }
225+
226+ if ( potentialLegacyIds . size === 0 ) {
227+ return { blocks, migrated : false }
228+ }
229+
230+ const rows = await db
231+ . select ( { id : credential . id , accountId : credential . accountId } )
232+ . from ( credential )
233+ . where ( inArray ( credential . accountId , [ ...potentialLegacyIds ] ) )
234+
235+ if ( rows . length === 0 ) {
236+ return { blocks, migrated : false }
237+ }
238+
239+ const accountToCredential = new Map ( rows . map ( ( r ) => [ r . accountId ! , r . id ] ) )
240+
241+ const migratedBlocks = Object . fromEntries (
242+ Object . entries ( blocks ) . map ( ( [ blockId , block ] ) => {
243+ let blockChanged = false
244+ const newSubBlocks = { ...block . subBlocks }
245+
246+ for ( const [ subBlockId , subBlock ] of Object . entries ( newSubBlocks ) ) {
247+ if ( CREDENTIAL_SUBBLOCK_IDS . has ( subBlockId ) && typeof subBlock . value === 'string' ) {
248+ const newId = accountToCredential . get ( subBlock . value )
249+ if ( newId ) {
250+ newSubBlocks [ subBlockId ] = { ...subBlock , value : newId }
251+ blockChanged = true
252+ }
253+ }
254+
255+ if ( subBlockId === 'tools' && Array . isArray ( subBlock . value ) ) {
256+ let toolsChanged = false
257+ const newTools = ( subBlock . value as any [ ] ) . map ( ( tool : any ) => {
258+ const credParam = tool ?. params ?. credential
259+ if ( typeof credParam === 'string' ) {
260+ const newId = accountToCredential . get ( credParam )
261+ if ( newId ) {
262+ toolsChanged = true
263+ return { ...tool , params : { ...tool . params , credential : newId } }
264+ }
265+ }
266+ return tool
267+ } )
268+ if ( toolsChanged ) {
269+ newSubBlocks [ subBlockId ] = { ...subBlock , value : newTools as any }
270+ blockChanged = true
271+ }
272+ }
273+ }
274+
275+ return [ blockId , blockChanged ? { ...block , subBlocks : newSubBlocks } : block ]
276+ } )
277+ )
278+
279+ return { blocks : migratedBlocks , migrated : true }
280+ }
281+
188282/**
189283 * Load workflow state from normalized tables
190284 * Returns null if no data found (fallback to JSON blob)
@@ -236,9 +330,31 @@ export async function loadWorkflowFromNormalizedTables(
236330 const { blocks : sanitizedBlocks } = sanitizeAgentToolsInBlocks ( blocksMap )
237331
238332 // Migrate old agent block format (systemPrompt/userPrompt) to new messages array format
239- // This ensures backward compatibility for workflows created before the messages-input refactor
240333 const migratedBlocks = migrateAgentBlocksToMessagesFormat ( sanitizedBlocks )
241334
335+ // Migrate legacy account.id → credential.id in OAuth subblocks
336+ const { blocks : credMigratedBlocks , migrated : credentialsMigrated } =
337+ await migrateCredentialIds ( migratedBlocks )
338+
339+ if ( credentialsMigrated ) {
340+ Promise . resolve ( ) . then ( async ( ) => {
341+ try {
342+ for ( const [ blockId , block ] of Object . entries ( credMigratedBlocks ) ) {
343+ if ( block . subBlocks !== migratedBlocks [ blockId ] ?. subBlocks ) {
344+ await db
345+ . update ( workflowBlocks )
346+ . set ( { subBlocks : block . subBlocks , updatedAt : new Date ( ) } )
347+ . where (
348+ and ( eq ( workflowBlocks . id , blockId ) , eq ( workflowBlocks . workflowId , workflowId ) )
349+ )
350+ }
351+ }
352+ } catch ( err ) {
353+ logger . warn ( 'Failed to persist credential ID migration' , { workflowId, error : err } )
354+ }
355+ } )
356+ }
357+
242358 // Convert edges to the expected format
243359 const edgesArray : Edge [ ] = edges . map ( ( edge ) => ( {
244360 id : edge . id ,
@@ -275,15 +391,13 @@ export async function loadWorkflowFromNormalizedTables(
275391 forEachItems : ( config as Loop ) . forEachItems ?? '' ,
276392 whileCondition : ( config as Loop ) . whileCondition ?? '' ,
277393 doWhileCondition : ( config as Loop ) . doWhileCondition ?? '' ,
278- enabled : migratedBlocks [ subflow . id ] ?. enabled ?? true ,
394+ enabled : credMigratedBlocks [ subflow . id ] ?. enabled ?? true ,
279395 }
280396 loops [ subflow . id ] = loop
281397
282- // Sync block.data with loop config to ensure all fields are present
283- // This allows switching between loop types without losing data
284- if ( migratedBlocks [ subflow . id ] ) {
285- const block = migratedBlocks [ subflow . id ]
286- migratedBlocks [ subflow . id ] = {
398+ if ( credMigratedBlocks [ subflow . id ] ) {
399+ const block = credMigratedBlocks [ subflow . id ]
400+ credMigratedBlocks [ subflow . id ] = {
287401 ...block ,
288402 data : {
289403 ...block . data ,
@@ -304,7 +418,7 @@ export async function loadWorkflowFromNormalizedTables(
304418 ( config as Parallel ) . parallelType === 'collection'
305419 ? ( config as Parallel ) . parallelType
306420 : 'count' ,
307- enabled : migratedBlocks [ subflow . id ] ?. enabled ?? true ,
421+ enabled : credMigratedBlocks [ subflow . id ] ?. enabled ?? true ,
308422 }
309423 parallels [ subflow . id ] = parallel
310424 } else {
@@ -313,7 +427,7 @@ export async function loadWorkflowFromNormalizedTables(
313427 } )
314428
315429 return {
316- blocks : migratedBlocks ,
430+ blocks : credMigratedBlocks ,
317431 edges : edgesArray ,
318432 loops,
319433 parallels,
0 commit comments