feat: add-create-update-record in workflow (#14654)

## Description

- this PR focuses on issue
https://github.com/twentyhq/core-team-issues/issues/1476
- Added upsert action


## Visual Appearance

<img width="1792" height="1041" alt="Screenshot 2025-10-03 at 12 57
58 PM"
src="https://github.com/user-attachments/assets/57afb96c-d4b3-4a87-95f0-11ac4bd61dd8"
/>




<img width="1792" height="1031" alt="Screenshot 2025-10-03 at 12 57
48 PM"
src="https://github.com/user-attachments/assets/9032d4c2-f0d2-46f1-8682-a7e5c280a303"
/>

---------

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
Co-authored-by: Thomas Trompette <thomas.trompette@sfr.fr>
This commit is contained in:
Harshit Singh
2025-10-21 19:18:25 +05:30
committed by GitHub
parent f5f23a9d31
commit 27f50c4f4e
41 changed files with 879 additions and 268 deletions
@@ -17,6 +17,7 @@ import { CreateRecordWorkflowAction } from 'src/modules/workflow/workflow-execut
import { DeleteRecordWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/delete-record.workflow-action';
import { FindRecordsWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/find-records.workflow-action';
import { UpdateRecordWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/update-record.workflow-action';
import { UpsertRecordWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/upsert-record.workflow-action';
import { ToolExecutorWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/tool-executor-workflow-action';
import { WorkflowActionType } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
@@ -25,6 +26,7 @@ export class WorkflowActionFactory {
constructor(
private readonly codeWorkflowAction: CodeWorkflowAction,
private readonly createRecordWorkflowAction: CreateRecordWorkflowAction,
private readonly upsertRecordWorkflowAction: UpsertRecordWorkflowAction,
private readonly updateRecordWorkflowAction: UpdateRecordWorkflowAction,
private readonly deleteRecordWorkflowAction: DeleteRecordWorkflowAction,
private readonly findRecordsWorkflowAction: FindRecordsWorkflowAction,
@@ -45,6 +47,8 @@ export class WorkflowActionFactory {
return this.toolExecutorWorkflowAction;
case WorkflowActionType.CREATE_RECORD:
return this.createRecordWorkflowAction;
case WorkflowActionType.UPSERT_RECORD:
return this.upsertRecordWorkflowAction;
case WorkflowActionType.UPDATE_RECORD:
return this.updateRecordWorkflowAction;
case WorkflowActionType.DELETE_RECORD:
@@ -0,0 +1,11 @@
import {
type WorkflowAction,
WorkflowActionType,
type WorkflowUpsertRecordAction,
} from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
export const isWorkflowUpsertRecordAction = (
action: WorkflowAction,
): action is WorkflowUpsertRecordAction => {
return action.type === WorkflowActionType.UPSERT_RECORD;
};
@@ -4,11 +4,13 @@ import { RecordCrudModule } from 'src/engine/core-modules/record-crud/record-cru
import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module';
import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module';
import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory';
import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-common.module';
import { WorkflowExecutionContextService } from 'src/modules/workflow/workflow-executor/services/workflow-execution-context.service';
import { CreateRecordWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/create-record.workflow-action';
import { DeleteRecordWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/delete-record.workflow-action';
import { FindRecordsWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/find-records.workflow-action';
import { UpdateRecordWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/update-record.workflow-action';
import { UpsertRecordWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/upsert-record.workflow-action';
import { WorkflowRunModule } from 'src/modules/workflow/workflow-runner/workflow-run/workflow-run.module';
@Module({
@@ -17,17 +19,20 @@ import { WorkflowRunModule } from 'src/modules/workflow/workflow-runner/workflow
WorkflowRunModule,
UserWorkspaceModule,
UserRoleModule,
WorkflowCommonModule,
],
providers: [
ScopedWorkspaceContextFactory,
WorkflowExecutionContextService,
CreateRecordWorkflowAction,
UpsertRecordWorkflowAction,
UpdateRecordWorkflowAction,
DeleteRecordWorkflowAction,
FindRecordsWorkflowAction,
],
exports: [
CreateRecordWorkflowAction,
UpsertRecordWorkflowAction,
UpdateRecordWorkflowAction,
DeleteRecordWorkflowAction,
FindRecordsWorkflowAction,
@@ -8,6 +8,7 @@ import { type ObjectRecordProperties } from 'src/engine/core-modules/record-crud
export type WorkflowCreateRecordActionInput = {
objectName: string;
objectRecord: ObjectRecordProperties;
upsert?: boolean;
};
export type WorkflowUpdateRecordActionInput = {
@@ -35,3 +36,9 @@ export type WorkflowFindRecordsActionInput = {
orderBy?: Partial<ObjectRecordOrderBy>;
limit?: number;
};
export type WorkflowUpsertRecordActionInput = {
objectName: string;
objectRecord: ObjectRecordProperties;
fieldsToUpdate?: string[];
};
@@ -1,4 +1,5 @@
import {
type WorkflowUpsertRecordActionInput,
type WorkflowCreateRecordActionInput,
type WorkflowDeleteRecordActionInput,
type WorkflowFindRecordsActionInput,
@@ -21,3 +22,7 @@ export type WorkflowDeleteRecordActionSettings = BaseWorkflowActionSettings & {
export type WorkflowFindRecordsActionSettings = BaseWorkflowActionSettings & {
input: WorkflowFindRecordsActionInput;
};
export type WorkflowUpsertRecordActionSettings = BaseWorkflowActionSettings & {
input: WorkflowUpsertRecordActionInput;
};
@@ -0,0 +1,93 @@
import { Injectable } from '@nestjs/common';
import { isDefined, resolveInput } from 'twenty-shared/utils';
import { type WorkflowAction } from 'src/modules/workflow/workflow-executor/interfaces/workflow-action.interface';
import {
RecordCrudException,
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 { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory';
import {
WorkflowStepExecutorException,
WorkflowStepExecutorExceptionCode,
} from 'src/modules/workflow/workflow-executor/exceptions/workflow-step-executor.exception';
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 { findStepOrThrow } from 'src/modules/workflow/workflow-executor/utils/find-step-or-throw.util';
import { isWorkflowUpsertRecordAction } from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/guards/is-workflow-upsert-record-action.guard';
import { WorkflowUpsertRecordActionInput } from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/types/workflow-record-crud-action-input.type';
@Injectable()
export class UpsertRecordWorkflowAction implements WorkflowAction {
constructor(
private readonly upsertRecordService: UpsertRecordService,
private readonly scopedWorkspaceContextFactory: ScopedWorkspaceContextFactory,
private readonly workflowExecutionContextService: WorkflowExecutionContextService,
) {}
async execute({
currentStepId,
steps,
context,
runInfo,
}: WorkflowActionInput): Promise<WorkflowActionOutput> {
const step = findStepOrThrow({
steps,
stepId: currentStepId,
});
if (!isWorkflowUpsertRecordAction(step)) {
throw new WorkflowStepExecutorException(
'Step is not an upsert record action',
WorkflowStepExecutorExceptionCode.INVALID_STEP_TYPE,
);
}
const workflowActionInput = resolveInput(
step.settings.input,
context,
) as WorkflowUpsertRecordActionInput;
if (!isDefined(workflowActionInput.objectName)) {
throw new RecordCrudException(
'Failed to upsert: Object name is required',
RecordCrudExceptionCode.INVALID_REQUEST,
);
}
const { workspaceId } = this.scopedWorkspaceContextFactory.create();
if (!workspaceId) {
throw new RecordCrudException(
'Failed to update: Workspace ID is required',
RecordCrudExceptionCode.INVALID_REQUEST,
);
}
const executionContext =
await this.workflowExecutionContextService.getExecutionContext(runInfo);
const toolOutput = await this.upsertRecordService.execute({
objectName: workflowActionInput.objectName,
objectRecord: workflowActionInput.objectRecord,
fieldsToUpdate: workflowActionInput.fieldsToUpdate,
workspaceId,
rolePermissionConfig: executionContext.rolePermissionConfig,
});
if (!toolOutput.success) {
throw new RecordCrudException(
toolOutput.error || toolOutput.message,
RecordCrudExceptionCode.RECORD_UPSERT_FAILED,
);
}
return {
result: toolOutput.result,
};
}
}
@@ -12,6 +12,7 @@ import {
type WorkflowDeleteRecordActionSettings,
type WorkflowFindRecordsActionSettings,
type WorkflowUpdateRecordActionSettings,
type WorkflowUpsertRecordActionSettings,
} from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/types/workflow-record-crud-action-settings.type';
export type BaseWorkflowActionSettings = {
@@ -32,6 +33,7 @@ export type WorkflowActionSettings =
| WorkflowCreateRecordActionSettings
| WorkflowUpdateRecordActionSettings
| WorkflowDeleteRecordActionSettings
| WorkflowUpsertRecordActionSettings
| WorkflowFindRecordsActionSettings
| WorkflowFormActionSettings
| WorkflowFilterActionSettings
@@ -6,6 +6,7 @@ import { type WorkflowHttpRequestActionSettings } from 'src/modules/workflow/wor
import { type WorkflowIteratorActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/iterator/types/workflow-iterator-action-settings.type';
import { type WorkflowSendEmailActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/mail-sender/types/workflow-send-email-action-settings.type';
import {
type WorkflowUpsertRecordActionSettings,
type WorkflowCreateRecordActionSettings,
type WorkflowDeleteRecordActionSettings,
type WorkflowFindRecordsActionSettings,
@@ -20,6 +21,7 @@ export enum WorkflowActionType {
CREATE_RECORD = 'CREATE_RECORD',
UPDATE_RECORD = 'UPDATE_RECORD',
DELETE_RECORD = 'DELETE_RECORD',
UPSERT_RECORD = 'UPSERT_RECORD',
FIND_RECORDS = 'FIND_RECORDS',
FORM = 'FORM',
FILTER = 'FILTER',
@@ -68,6 +70,11 @@ export type WorkflowDeleteRecordAction = BaseWorkflowAction & {
settings: WorkflowDeleteRecordActionSettings;
};
export type WorkflowUpsertRecordAction = BaseWorkflowAction & {
type: WorkflowActionType.UPSERT_RECORD;
settings: WorkflowUpsertRecordActionSettings;
};
export type WorkflowFindRecordsAction = BaseWorkflowAction & {
type: WorkflowActionType.FIND_RECORDS;
settings: WorkflowFindRecordsActionSettings;
@@ -113,6 +120,7 @@ export type WorkflowAction =
| WorkflowCreateRecordAction
| WorkflowUpdateRecordAction
| WorkflowDeleteRecordAction
| WorkflowUpsertRecordAction
| WorkflowFindRecordsAction
| WorkflowFormAction
| WorkflowFilterAction