Decouple Send Email node from workflows (#13322)

- Renamed `WorkflowActionAdapter` to `ToolExecutorWorkflowAction`
- Renamed `settingPermission` table to `permissionFlag` and `setting`
column to `flag`
- Decoupled the send email logic from workflows to tools
- Add new `Tools Permission` section in FE

---------

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
Abdul Rahman
2025-07-24 16:01:33 +05:30
committed by GitHub
parent eb404478c3
commit e93adde4b8
98 changed files with 1076 additions and 705 deletions
@@ -10,26 +10,24 @@ import { AiAgentWorkflowAction } from 'src/modules/workflow/workflow-executor/wo
import { CodeWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/code/code.workflow-action';
import { FilterWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/filter/filter.workflow-action';
import { FormWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/form/form.workflow-action';
import { SendEmailWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/mail-sender/send-email.workflow-action';
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 { 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';
import { WorkflowActionAdapter } from 'src/modules/workflow/workflow-executor/workflow-actions/workflow-action-adapter';
@Injectable()
export class WorkflowActionFactory {
constructor(
private readonly codeWorkflowAction: CodeWorkflowAction,
private readonly sendEmailWorkflowAction: SendEmailWorkflowAction,
private readonly createRecordWorkflowAction: CreateRecordWorkflowAction,
private readonly updateRecordWorkflowAction: UpdateRecordWorkflowAction,
private readonly deleteRecordWorkflowAction: DeleteRecordWorkflowAction,
private readonly findRecordsWorkflowAction: FindRecordsWorkflowAction,
private readonly formWorkflowAction: FormWorkflowAction,
private readonly filterWorkflowAction: FilterWorkflowAction,
private readonly workflowActionAdapter: WorkflowActionAdapter,
private readonly toolExecutorWorkflowAction: ToolExecutorWorkflowAction,
private readonly aiAgentWorkflowAction: AiAgentWorkflowAction,
) {}
@@ -38,7 +36,7 @@ export class WorkflowActionFactory {
case WorkflowActionType.CODE:
return this.codeWorkflowAction;
case WorkflowActionType.SEND_EMAIL:
return this.sendEmailWorkflowAction;
return this.toolExecutorWorkflowAction;
case WorkflowActionType.CREATE_RECORD:
return this.createRecordWorkflowAction;
case WorkflowActionType.UPDATE_RECORD:
@@ -52,7 +50,7 @@ export class WorkflowActionFactory {
case WorkflowActionType.FILTER:
return this.filterWorkflowAction;
case WorkflowActionType.HTTP_REQUEST:
return this.workflowActionAdapter;
return this.toolExecutorWorkflowAction;
case WorkflowActionType.AI_AGENT:
return this.aiAgentWorkflowAction;
default:
@@ -1,12 +0,0 @@
import { Module } from '@nestjs/common';
import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory';
import { MessagingImportManagerModule } from 'src/modules/messaging/message-import-manager/messaging-import-manager.module';
import { SendEmailWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/mail-sender/send-email.workflow-action';
@Module({
imports: [MessagingImportManagerModule],
providers: [ScopedWorkspaceContextFactory, SendEmailWorkflowAction],
exports: [SendEmailWorkflowAction],
})
export class SendEmailActionModule {}
@@ -1,140 +0,0 @@
import { Injectable, Logger } from '@nestjs/common';
import DOMPurify from 'dompurify';
import { JSDOM } from 'jsdom';
import { isDefined, isValidUuid } from 'twenty-shared/utils';
import { z } from 'zod';
import { WorkflowAction } from 'src/modules/workflow/workflow-executor/interfaces/workflow-action.interface';
import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { MessagingSendMessageService } from 'src/modules/messaging/message-import-manager/services/messaging-send-message.service';
import {
WorkflowStepExecutorException,
WorkflowStepExecutorExceptionCode,
} from 'src/modules/workflow/workflow-executor/exceptions/workflow-step-executor.exception';
import { WorkflowActionInput } from 'src/modules/workflow/workflow-executor/types/workflow-action-input';
import { WorkflowActionOutput } from 'src/modules/workflow/workflow-executor/types/workflow-action-output.type';
import { resolveInput } from 'src/modules/workflow/workflow-executor/utils/variable-resolver.util';
import {
SendEmailActionException,
SendEmailActionExceptionCode,
} from 'src/modules/workflow/workflow-executor/workflow-actions/mail-sender/exceptions/send-email-action.exception';
import { isWorkflowSendEmailAction } from 'src/modules/workflow/workflow-executor/workflow-actions/mail-sender/guards/is-workflow-send-email-action.guard';
import { WorkflowSendEmailActionInput } from 'src/modules/workflow/workflow-executor/workflow-actions/mail-sender/types/workflow-send-email-action-input.type';
export type WorkflowSendEmailStepOutputSchema = {
success: boolean;
};
@Injectable()
export class SendEmailWorkflowAction implements WorkflowAction {
private readonly logger = new Logger(SendEmailWorkflowAction.name);
constructor(
private readonly scopedWorkspaceContextFactory: ScopedWorkspaceContextFactory,
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly sendMessageService: MessagingSendMessageService,
) {}
private async getConnectedAccount(connectedAccountId: string) {
if (!isValidUuid(connectedAccountId)) {
throw new SendEmailActionException(
`Connected Account ID is not a valid UUID`,
SendEmailActionExceptionCode.INVALID_CONNECTED_ACCOUNT_ID,
);
}
const { workspaceId } = this.scopedWorkspaceContextFactory.create();
if (!workspaceId) {
throw new WorkflowStepExecutorException(
'Scoped workspace not found',
WorkflowStepExecutorExceptionCode.SCOPED_WORKSPACE_NOT_FOUND,
);
}
const connectedAccountRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ConnectedAccountWorkspaceEntity>(
workspaceId,
'connectedAccount',
);
const connectedAccount = await connectedAccountRepository.findOneBy({
id: connectedAccountId,
});
if (!isDefined(connectedAccount)) {
throw new SendEmailActionException(
`Connected Account '${connectedAccountId}' not found`,
SendEmailActionExceptionCode.CONNECTED_ACCOUNT_NOT_FOUND,
);
}
return connectedAccount;
}
async execute({
currentStepId,
steps,
context,
}: WorkflowActionInput): Promise<WorkflowActionOutput> {
const step = steps.find((step) => step.id === currentStepId);
if (!step) {
throw new WorkflowStepExecutorException(
'Step not found',
WorkflowStepExecutorExceptionCode.STEP_NOT_FOUND,
);
}
if (!isWorkflowSendEmailAction(step)) {
throw new WorkflowStepExecutorException(
'Step is not a send email action',
WorkflowStepExecutorExceptionCode.INVALID_STEP_TYPE,
);
}
const connectedAccount = await this.getConnectedAccount(
step.settings.input.connectedAccountId,
);
const workflowActionInput = resolveInput(
step.settings.input,
context,
) as WorkflowSendEmailActionInput;
const { email, body, subject } = workflowActionInput;
const emailSchema = z.string().trim().email('Invalid email');
const result = emailSchema.safeParse(email);
if (!result.success) {
throw new SendEmailActionException(
`Email '${email}' invalid`,
SendEmailActionExceptionCode.INVALID_EMAIL,
);
}
const window = new JSDOM('').window;
const purify = DOMPurify(window);
const safeBody = purify.sanitize(body || '');
const safeSubject = purify.sanitize(subject || '');
await this.sendMessageService.sendMessage(
{
to: email,
subject: safeSubject,
body: safeBody,
},
connectedAccount,
);
this.logger.log(`Email sent successfully`);
return {
result: { success: true } satisfies WorkflowSendEmailStepOutputSchema,
};
}
}
@@ -2,8 +2,8 @@ import { Injectable } from '@nestjs/common';
import { WorkflowAction } from 'src/modules/workflow/workflow-executor/interfaces/workflow-action.interface';
import { TOOLS } from 'src/engine/core-modules/tool/constants/tools.const';
import { ToolType } from 'src/engine/core-modules/tool/enums/tool-type.enum';
import { ToolRegistryService } from 'src/engine/core-modules/tool/services/tool-registry.service';
import { ToolInput } from 'src/engine/core-modules/tool/types/tool-input.type';
import { WorkflowActionInput } from 'src/modules/workflow/workflow-executor/types/workflow-action-input';
import { WorkflowActionOutput } from 'src/modules/workflow/workflow-executor/types/workflow-action-output.type';
@@ -11,7 +11,9 @@ import { resolveInput } from 'src/modules/workflow/workflow-executor/utils/varia
import { WorkflowActionType } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
@Injectable()
export class WorkflowActionAdapter implements WorkflowAction {
export class ToolExecutorWorkflowAction implements WorkflowAction {
constructor(private readonly toolRegistry: ToolRegistryService) {}
async execute({
currentStepId,
steps,
@@ -31,7 +33,7 @@ export class WorkflowActionAdapter implements WorkflowAction {
);
}
const tool = TOOLS.get(toolType);
const tool = this.toolRegistry.getTool(toolType);
if (!tool) {
throw new Error(
@@ -54,6 +56,7 @@ export class WorkflowActionAdapter implements WorkflowAction {
): ToolType | null {
const mapping: Partial<Record<WorkflowActionType, ToolType>> = {
[WorkflowActionType.HTTP_REQUEST]: ToolType.HTTP_REQUEST,
[WorkflowActionType.SEND_EMAIL]: ToolType.SEND_EMAIL,
};
return mapping[actionType] || null;
@@ -1,5 +1,6 @@
import { Module } from '@nestjs/common';
import { AiModule } from 'src/engine/core-modules/ai/ai.module';
import { BillingModule } from 'src/engine/core-modules/billing/billing.module';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory';
@@ -9,9 +10,8 @@ import { AiAgentActionModule } from 'src/modules/workflow/workflow-executor/work
import { CodeActionModule } from 'src/modules/workflow/workflow-executor/workflow-actions/code/code-action.module';
import { FilterActionModule } from 'src/modules/workflow/workflow-executor/workflow-actions/filter/filter-action.module';
import { FormActionModule } from 'src/modules/workflow/workflow-executor/workflow-actions/form/form-action.module';
import { SendEmailActionModule } from 'src/modules/workflow/workflow-executor/workflow-actions/mail-sender/send-email-action.module';
import { RecordCRUDActionModule } from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/record-crud-action.module';
import { WorkflowActionAdapter } from 'src/modules/workflow/workflow-executor/workflow-actions/workflow-action-adapter';
import { ToolExecutorWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/tool-executor-workflow-action';
import { WorkflowExecutorWorkspaceService } from 'src/modules/workflow/workflow-executor/workspace-services/workflow-executor.workspace-service';
import { WorkflowRunModule } from 'src/modules/workflow/workflow-runner/workflow-run/workflow-run.module';
@@ -19,7 +19,6 @@ import { WorkflowRunModule } from 'src/modules/workflow/workflow-runner/workflow
imports: [
WorkflowCommonModule,
CodeActionModule,
SendEmailActionModule,
RecordCRUDActionModule,
FormActionModule,
WorkflowRunModule,
@@ -27,12 +26,13 @@ import { WorkflowRunModule } from 'src/modules/workflow/workflow-runner/workflow
FilterActionModule,
AiAgentActionModule,
FeatureFlagModule,
AiModule,
],
providers: [
WorkflowExecutorWorkspaceService,
ScopedWorkspaceContextFactory,
WorkflowActionFactory,
WorkflowActionAdapter,
ToolExecutorWorkflowAction,
],
exports: [WorkflowExecutorWorkspaceService],
})