From 79e0207efa6b048e8d1a1ac78f607d9067762b8b Mon Sep 17 00:00:00 2001 From: Thomas Trompette Date: Wed, 14 Jan 2026 15:43:35 +0100 Subject: [PATCH] Filter valid fields in record steps (#17145) Fixes https://github.com/twentyhq/twenty/issues/16775 When a field is deleted from the model, the workflow action step still stores the deleted field name in `step.settings.input.objectRecord`. We need to filter out the fields that are not valid anymore. This logic existed before but had been removed with the migration to tool services. --- .../filter-valid-fields-in-record.util.ts | 30 +++++++++++++++++ .../create-record.workflow-action.ts | 9 ++++- .../update-record.workflow-action.ts | 22 +++++++++++-- .../upsert-record.workflow-action.ts | 33 +++++++++++++++++-- 4 files changed, 89 insertions(+), 5 deletions(-) create mode 100644 packages/twenty-server/src/modules/workflow/workflow-executor/utils/filter-valid-fields-in-record.util.ts diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/utils/filter-valid-fields-in-record.util.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/utils/filter-valid-fields-in-record.util.ts new file mode 100644 index 0000000000..7c3706e027 --- /dev/null +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/utils/filter-valid-fields-in-record.util.ts @@ -0,0 +1,30 @@ +import { isDefined } from 'twenty-shared/utils'; + +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type'; +import { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/build-field-maps-from-flat-object-metadata.util'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; + +export const filterValidFieldsInRecord = ( + record: Record, + flatObjectMetadata: FlatObjectMetadata, + flatFieldMetadataMaps: FlatEntityMaps, +): Record => { + const { fieldIdByName, fieldIdByJoinColumnName } = + buildFieldMapsFromFlatObjectMetadata( + flatFieldMetadataMaps, + flatObjectMetadata, + ); + + const filteredRecord: Record = {}; + + for (const [key, value] of Object.entries(record)) { + const fieldMetadataId = fieldIdByName[key] || fieldIdByJoinColumnName[key]; + + if (isDefined(fieldMetadataId)) { + filteredRecord[key] = value; + } + } + + return filteredRecord; +}; diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/create-record.workflow-action.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/create-record.workflow-action.ts index ed5570038f..1c7585d97c 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/create-record.workflow-action.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/create-record.workflow-action.ts @@ -14,6 +14,7 @@ import { WorkflowExecutionContextService } from 'src/modules/workflow/workflow-e import { type WorkflowActionInput } from 'src/modules/workflow/workflow-executor/types/workflow-action-input'; import { type WorkflowActionOutput } from 'src/modules/workflow/workflow-executor/types/workflow-action-output.type'; import { buildWorkflowActorMetadata } from 'src/modules/workflow/workflow-executor/utils/build-workflow-actor-metadata.util'; +import { filterValidFieldsInRecord } from 'src/modules/workflow/workflow-executor/utils/filter-valid-fields-in-record.util'; import { findStepOrThrow } from 'src/modules/workflow/workflow-executor/utils/find-step-or-throw.util'; import { resolveRichTextFieldsInRecord } from 'src/modules/workflow/workflow-executor/utils/resolve-rich-text-fields-in-record.util'; import { type WorkflowCreateRecordActionInput } from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/types/workflow-record-crud-action-input.type'; @@ -61,6 +62,12 @@ export class CreateRecordWorkflowAction implements WorkflowAction { context, ) as WorkflowCreateRecordActionInput; + const filteredObjectRecord = filterValidFieldsInRecord( + workflowActionInput.objectRecord, + objectMetadataInfo.flatObjectMetadata, + objectMetadataInfo.flatFieldMetadataMaps, + ); + const executionContext = await this.workflowExecutionContextService.getExecutionContext(runInfo); @@ -68,7 +75,7 @@ export class CreateRecordWorkflowAction implements WorkflowAction { const toolOutput = await this.createRecordService.execute({ objectName: workflowActionInput.objectName, - objectRecord: workflowActionInput.objectRecord, + objectRecord: filteredObjectRecord, authContext: executionContext.authContext, createdBy, rolePermissionConfig: executionContext.rolePermissionConfig, diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/update-record.workflow-action.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/update-record.workflow-action.ts index b37e8e470a..9e5772c498 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/update-record.workflow-action.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/update-record.workflow-action.ts @@ -18,6 +18,7 @@ import { WorkflowExecutionContextService } from 'src/modules/workflow/workflow-e import { type WorkflowActionInput } from 'src/modules/workflow/workflow-executor/types/workflow-action-input'; import { type WorkflowActionOutput } from 'src/modules/workflow/workflow-executor/types/workflow-action-output.type'; import { buildWorkflowActorMetadata } from 'src/modules/workflow/workflow-executor/utils/build-workflow-actor-metadata.util'; +import { filterValidFieldsInRecord } from 'src/modules/workflow/workflow-executor/utils/filter-valid-fields-in-record.util'; import { findStepOrThrow } from 'src/modules/workflow/workflow-executor/utils/find-step-or-throw.util'; import { resolveRichTextFieldsInRecord } from 'src/modules/workflow/workflow-executor/utils/resolve-rich-text-fields-in-record.util'; import { isWorkflowUpdateRecordAction } from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/guards/is-workflow-update-record-action.guard'; @@ -84,6 +85,23 @@ export class UpdateRecordWorkflowAction implements WorkflowAction { ); } + const filteredObjectRecord = filterValidFieldsInRecord( + workflowActionInput.objectRecord, + objectMetadataInfo.flatObjectMetadata, + objectMetadataInfo.flatFieldMetadataMaps, + ); + + const filteredFieldsToUpdate = workflowActionInput.fieldsToUpdate?.filter( + (fieldName) => fieldName in filteredObjectRecord, + ); + + if (filteredFieldsToUpdate?.length === 0) { + throw new RecordCrudException( + 'Failed to update: No fields to update', + RecordCrudExceptionCode.INVALID_REQUEST, + ); + } + const executionContext = await this.workflowExecutionContextService.getExecutionContext(runInfo); @@ -92,8 +110,8 @@ export class UpdateRecordWorkflowAction implements WorkflowAction { const toolOutput = await this.updateRecordService.execute({ objectName: workflowActionInput.objectName, objectRecordId: workflowActionInput.objectRecordId, - objectRecord: workflowActionInput.objectRecord, - fieldsToUpdate: workflowActionInput.fieldsToUpdate, + objectRecord: filteredObjectRecord, + fieldsToUpdate: filteredFieldsToUpdate, authContext: executionContext.authContext, updatedBy, rolePermissionConfig: executionContext.rolePermissionConfig, diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/upsert-record.workflow-action.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/upsert-record.workflow-action.ts index 369dc805cb..4b1d36bf56 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/upsert-record.workflow-action.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/upsert-record.workflow-action.ts @@ -9,6 +9,7 @@ import { RecordCrudExceptionCode, } from 'src/engine/core-modules/record-crud/exceptions/record-crud.exception'; import { UpsertRecordService } from 'src/engine/core-modules/record-crud/services/upsert-record.service'; +import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service'; import { WorkflowStepExecutorException, WorkflowStepExecutorExceptionCode, @@ -16,7 +17,9 @@ import { import { WorkflowExecutionContextService } from 'src/modules/workflow/workflow-executor/services/workflow-execution-context.service'; import { type WorkflowActionInput } from 'src/modules/workflow/workflow-executor/types/workflow-action-input'; import { type WorkflowActionOutput } from 'src/modules/workflow/workflow-executor/types/workflow-action-output.type'; +import { filterValidFieldsInRecord } from 'src/modules/workflow/workflow-executor/utils/filter-valid-fields-in-record.util'; import { findStepOrThrow } from 'src/modules/workflow/workflow-executor/utils/find-step-or-throw.util'; +import { resolveRichTextFieldsInRecord } from 'src/modules/workflow/workflow-executor/utils/resolve-rich-text-fields-in-record.util'; import { isWorkflowUpsertRecordAction } from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/guards/is-workflow-upsert-record-action.guard'; import { type WorkflowUpsertRecordActionInput } from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/types/workflow-record-crud-action-input.type'; @@ -25,6 +28,7 @@ export class UpsertRecordWorkflowAction implements WorkflowAction { constructor( private readonly upsertRecordService: UpsertRecordService, private readonly workflowExecutionContextService: WorkflowExecutionContextService, + private readonly workflowCommonWorkspaceService: WorkflowCommonWorkspaceService, ) {} async execute({ @@ -45,8 +49,27 @@ export class UpsertRecordWorkflowAction implements WorkflowAction { ); } + const { workspaceId } = runInfo; + + const rawInput = step.settings.input as WorkflowUpsertRecordActionInput; + + const objectMetadataInfo = + await this.workflowCommonWorkspaceService.getObjectMetadataInfo( + rawInput.objectName, + workspaceId, + ); + + const inputWithResolvedRichText = { + ...rawInput, + objectRecord: resolveRichTextFieldsInRecord( + rawInput.objectRecord, + objectMetadataInfo, + context, + ), + }; + const workflowActionInput = resolveInput( - step.settings.input, + inputWithResolvedRichText, context, ) as WorkflowUpsertRecordActionInput; @@ -57,12 +80,18 @@ export class UpsertRecordWorkflowAction implements WorkflowAction { ); } + const filteredObjectRecord = filterValidFieldsInRecord( + workflowActionInput.objectRecord, + objectMetadataInfo.flatObjectMetadata, + objectMetadataInfo.flatFieldMetadataMaps, + ); + const executionContext = await this.workflowExecutionContextService.getExecutionContext(runInfo); const toolOutput = await this.upsertRecordService.execute({ objectName: workflowActionInput.objectName, - objectRecord: workflowActionInput.objectRecord, + objectRecord: filteredObjectRecord, authContext: executionContext.authContext, rolePermissionConfig: executionContext.rolePermissionConfig, });