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.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22800?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
This commit is contained in:
Thomas Trompette
2026-07-10 16:21:04 +02:00
committed by GitHub
parent 165d47a9f0
commit cb2e325c4b
3 changed files with 54 additions and 25 deletions
@@ -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,
@@ -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<FlatObjectMetadata>,
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>,
) => {
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',
@@ -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)', () => {