From cb2e325c4bc9b83a0637d1a40a5e96e905bbaf44 Mon Sep 17 00:00:00 2001 From: Thomas Trompette Date: Fri, 10 Jul 2026 16:21:04 +0200 Subject: [PATCH] fix(workflow): make prefilled workflow ids unique per workspace (#22800) ## Problem \`prefillWorkflows\` (run for every workspace on \`activateWorkspace\`) inserts workflows and versions with **hardcoded ids** (\`QUICK_LEAD_WORKFLOW_ID = 8b213cac...\`, etc.). So every workspace carries the same workflow/version record ids. Within a workspace schema that's harmless, but it means workspace record ids are **not unique across workspaces**, which: - breaks the workflowVersion backfill on the shared core table (surfaced as the \`IDX_WORKFLOW_VERSION_ONE_ACTIVE_PER_WORKFLOW\` duplicate-key error, since multiple workspaces claim the same active \`workflowId\`), and - collides on \`core.workflow\`/\`core.workflowVersion\` PKs once workflows migrate to core (the core row reuses the workspace record id), causing cross-workspace clobbering. ## Fix Derive the prefill ids **deterministically per workspace**: \`getWorkflowPrefillIds(workspaceId)\` returns \`v5(label:workspaceId, namespace)\` for each of the workflow/version/trigger ids. Deterministic (stable across the idempotent \`orIgnore\` re-runs) but unique per workspace. The command-menu-item prefill uses the same helper so its \`workflowVersionId\` reference stays consistent. Only affects **new** workspaces; existing workspaces keep their current ids (prefill is skipped on re-activation). ## Test Reset seeds two workspaces; both now get a Quick Lead workflow with a **distinct** v5-derived id (not the old \`8b213cac\`), and internal references stay consistent (\`version.workflowId == workflow.id\`, \`lastPublishedVersionId == version.id\`). Typecheck + lint clean. Companion to #22795 (which scopes the active index to workspace). Together they fix the backfill duplicate-id failures. Review in cubic --- ...refill-workflow-command-menu-items.util.ts | 6 +- .../utils/prefill-workflows.util.ts | 63 +++++++++++++------ .../quick-lead-workflow.integration-spec.ts | 10 ++- 3 files changed, 54 insertions(+), 25 deletions(-) diff --git a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-workflow-command-menu-items.util.ts b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-workflow-command-menu-items.util.ts index cead263176..3ab4090cc2 100644 --- a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-workflow-command-menu-items.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-workflow-command-menu-items.util.ts @@ -6,7 +6,7 @@ import { CommandMenuItemAvailabilityType } from 'src/engine/metadata-modules/com import { EngineComponentKey } from 'src/engine/metadata-modules/command-menu-item/enums/engine-component-key.enum'; import { type FlatCommandMenuItem } from 'src/engine/metadata-modules/flat-command-menu-item/types/flat-command-menu-item.type'; import { type WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; -import { QUICK_LEAD_WORKFLOW_VERSION_ID } from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-workflows.util'; +import { getWorkflowPrefillIds } from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-workflows.util'; import { type WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service'; const QUICK_LEAD_COMMAND_MENU_ITEM_UNIVERSAL_IDENTIFIER = @@ -46,6 +46,8 @@ export const prefillWorkflowCommandMenuItems = async ({ const now = new Date().toISOString(); + const { quickLeadWorkflowVersionId } = getWorkflowPrefillIds(workspaceId); + const quickLeadFlatCommandMenuItem: FlatCommandMenuItem = { id: v4(), universalIdentifier: QUICK_LEAD_COMMAND_MENU_ITEM_UNIVERSAL_IDENTIFIER, @@ -53,7 +55,7 @@ export const prefillWorkflowCommandMenuItems = async ({ applicationUniversalIdentifier: workspaceCustomFlatApplication.universalIdentifier, workspaceId, - workflowVersionId: QUICK_LEAD_WORKFLOW_VERSION_ID, + workflowVersionId: quickLeadWorkflowVersionId, frontComponentId: null, frontComponentUniversalIdentifier: null, engineComponentKey: EngineComponentKey.TRIGGER_WORKFLOW_VERSION, diff --git a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-workflows.util.ts b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-workflows.util.ts index 842ea60152..89a609a429 100644 --- a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-workflows.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-workflows.util.ts @@ -1,6 +1,7 @@ import { FieldActorSource } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { type EntityManager } from 'typeorm'; +import { v5 } from 'uuid'; import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; @@ -13,15 +14,30 @@ import { generateFakeObjectRecordEvent } from 'src/modules/workflow/workflow-bui import { generateObjectRecordFields } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-object-record-fields'; import { getCreateCompanyWhenAddingNewPersonCodeStepLogicFunctionIds } from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-workflow-code-step-logic-functions.util'; -export const QUICK_LEAD_WORKFLOW_ID = '8b213cac-a68b-4ffe-817a-3ec994e9932d'; -export const QUICK_LEAD_WORKFLOW_VERSION_ID = - 'ac67974f-c524-4288-9d88-af8515400b68'; -export const CREATE_COMPANY_WHEN_ADDING_NEW_PERSON_WORKFLOW_ID = - '887c6c06-fbc5-4b45-8d6b-f7b6b0f40b12'; -export const CREATE_COMPANY_WHEN_ADDING_NEW_PERSON_WORKFLOW_VERSION_ID = - '0f276d7e-a950-41ab-ad98-35e80753dc58'; -export const CREATE_COMPANY_WHEN_ADDING_NEW_PERSON_AUTOMATED_TRIGGER_ID = - 'c54f5990-13a3-4c3b-b75d-df09e7843036'; +const WORKFLOW_PREFILL_ID_NAMESPACE = '8b213cac-a68b-4ffe-817a-3ec994e9932d'; + +export const getWorkflowPrefillIds = (workspaceId: string) => ({ + quickLeadWorkflowId: v5( + `quickLeadWorkflow:${workspaceId}`, + WORKFLOW_PREFILL_ID_NAMESPACE, + ), + quickLeadWorkflowVersionId: v5( + `quickLeadWorkflowVersion:${workspaceId}`, + WORKFLOW_PREFILL_ID_NAMESPACE, + ), + createCompanyWorkflowId: v5( + `createCompanyWorkflow:${workspaceId}`, + WORKFLOW_PREFILL_ID_NAMESPACE, + ), + createCompanyWorkflowVersionId: v5( + `createCompanyWorkflowVersion:${workspaceId}`, + WORKFLOW_PREFILL_ID_NAMESPACE, + ), + createCompanyAutomatedTriggerId: v5( + `createCompanyAutomatedTrigger:${workspaceId}`, + WORKFLOW_PREFILL_ID_NAMESPACE, + ), +}); export const prefillWorkflows = async ( entityManager: EntityManager, @@ -30,6 +46,14 @@ export const prefillWorkflows = async ( flatObjectMetadataMaps: FlatEntityMaps, flatFieldMetadataMaps: FlatEntityMaps, ) => { + const { + quickLeadWorkflowId, + quickLeadWorkflowVersionId, + createCompanyWorkflowId, + createCompanyWorkflowVersionId, + createCompanyAutomatedTriggerId, + } = getWorkflowPrefillIds(workspaceId); + const { extractDomainLogicFunctionId, findMatchingCompanyByDomainLogicFunctionId, @@ -112,9 +136,9 @@ export const prefillWorkflows = async ( .orIgnore() .values([ { - id: QUICK_LEAD_WORKFLOW_ID, + id: quickLeadWorkflowId, name: 'Quick Lead', - lastPublishedVersionId: QUICK_LEAD_WORKFLOW_VERSION_ID, + lastPublishedVersionId: quickLeadWorkflowVersionId, statuses: ['ACTIVE'], position: 1, createdBySource: FieldActorSource.SYSTEM, @@ -126,10 +150,9 @@ export const prefillWorkflows = async ( updatedByName: 'System', }, { - id: CREATE_COMPANY_WHEN_ADDING_NEW_PERSON_WORKFLOW_ID, + id: createCompanyWorkflowId, name: 'Create company when adding a new person', - lastPublishedVersionId: - CREATE_COMPANY_WHEN_ADDING_NEW_PERSON_WORKFLOW_VERSION_ID, + lastPublishedVersionId: createCompanyWorkflowVersionId, statuses: ['ACTIVE'], position: 2, createdBySource: FieldActorSource.SYSTEM, @@ -159,7 +182,7 @@ export const prefillWorkflows = async ( .orIgnore() .values([ { - id: QUICK_LEAD_WORKFLOW_VERSION_ID, + id: quickLeadWorkflowVersionId, name: 'v1', trigger: JSON.stringify({ name: 'Launch manually', @@ -354,10 +377,10 @@ export const prefillWorkflows = async ( ]), status: 'ACTIVE', position: 1, - workflowId: QUICK_LEAD_WORKFLOW_ID, + workflowId: quickLeadWorkflowId, }, { - id: CREATE_COMPANY_WHEN_ADDING_NEW_PERSON_WORKFLOW_VERSION_ID, + id: createCompanyWorkflowVersionId, name: 'v1', trigger: JSON.stringify({ name: 'Record is created or updated', @@ -749,7 +772,7 @@ export const prefillWorkflows = async ( ]), status: 'ACTIVE', position: 2, - workflowId: CREATE_COMPANY_WHEN_ADDING_NEW_PERSON_WORKFLOW_ID, + workflowId: createCompanyWorkflowId, }, ]) .returning('*') @@ -767,8 +790,8 @@ export const prefillWorkflows = async ( .orIgnore() .values([ { - id: CREATE_COMPANY_WHEN_ADDING_NEW_PERSON_AUTOMATED_TRIGGER_ID, - workflowId: CREATE_COMPANY_WHEN_ADDING_NEW_PERSON_WORKFLOW_ID, + id: createCompanyAutomatedTriggerId, + workflowId: createCompanyWorkflowId, type: 'DATABASE_EVENT', settings: { eventName: 'person.upserted', diff --git a/packages/twenty-server/test/integration/graphql/suites/workflow/quick-lead-workflow.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/workflow/quick-lead-workflow.integration-spec.ts index 4a1bb1fec4..2c5f7de381 100644 --- a/packages/twenty-server/test/integration/graphql/suites/workflow/quick-lead-workflow.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/workflow/quick-lead-workflow.integration-spec.ts @@ -8,11 +8,15 @@ import { } from 'test/integration/graphql/suites/workflow/utils/workflow-run-test.util'; import { v4 as uuidv4 } from 'uuid'; +import { SEED_APPLE_WORKSPACE_ID } from 'src/engine/workspace-manager/dev-seeder/core/constants/seeder-workspaces.constant'; +import { getWorkflowPrefillIds } from 'src/engine/workspace-manager/standard-objects-prefill-data/utils/prefill-workflows.util'; + const client = request(`http://localhost:${APP_PORT}`); -// Quick Lead workflow IDs from prefill-workflows.ts -const QUICK_LEAD_WORKFLOW_ID = '8b213cac-a68b-4ffe-817a-3ec994e9932d'; -const QUICK_LEAD_WORKFLOW_VERSION_ID = 'ac67974f-c524-4288-9d88-af8515400b68'; +const { + quickLeadWorkflowId: QUICK_LEAD_WORKFLOW_ID, + quickLeadWorkflowVersionId: QUICK_LEAD_WORKFLOW_VERSION_ID, +} = getWorkflowPrefillIds(SEED_APPLE_WORKSPACE_ID); const FORM_STEP_ID = '6e089bc9-aabd-435f-865f-f31c01c8f4a7'; describe('Quick Lead Workflow (e2e)', () => {