Feature - HTTP request node (#12509)

Closes [#1072](https://github.com/twentyhq/core-team-issues/issues/1072)



https://github.com/user-attachments/assets/adff3474-6ec3-4369-a0c8-fb4be7defe85

---------

Co-authored-by: Raphaël Bosi <71827178+bosiraphael@users.noreply.github.com>
Co-authored-by: etiennejouan <jouan.etienne@gmail.com>
Co-authored-by: Guillim <guillim@users.noreply.github.com>
Co-authored-by: guillim <guigloo@msn.com>
Co-authored-by: prastoin <paul@twenty.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
Co-authored-by: Thomas des Francs <tdesfrancs@gmail.com>
Co-authored-by: martmull <martmull@hotmail.fr>
Co-authored-by: nitin <142569587+ehconitin@users.noreply.github.com>
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
Co-authored-by: Félix Malfait <felix@twenty.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: Marie <51697796+ijreilly@users.noreply.github.com>
Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com>
Co-authored-by: Jordan Chalupka <9794216+jordan-chalupka@users.noreply.github.com>
Co-authored-by: Thomas Trompette <thomas.trompette@sfr.fr>
Co-authored-by: jaspass04 <147055860+jaspass04@users.noreply.github.com>
Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@twenty.com>
Co-authored-by: Weiko <corentin@twenty.com>
Co-authored-by: Matt Dvertola <64113801+mdvertola@users.noreply.github.com>
Co-authored-by: Zeroday BYTE <github@zerodaysec.org>
Co-authored-by: Naifer <161821705+omarNaifer12@users.noreply.github.com>
Co-authored-by: Karuna Tata <karuna.tata@devrev.ai>
Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
Co-authored-by: Ajay A Adsule <103304466+AjayAdsule@users.noreply.github.com>
Co-authored-by: Baptiste Devessier <baptiste@devessier.fr>
Co-authored-by: oliver <8559757+oliverqx@users.noreply.github.com>
Co-authored-by: Ahmad Zaheer <55204917+ahmadzaheer-dev@users.noreply.github.com>
Co-authored-by: Paul Rastoin <45004772+prastoin@users.noreply.github.com>
This commit is contained in:
Abdul Rahman
2025-06-13 17:11:22 +05:30
committed by GitHub
parent e9733ea33a
commit 19b7ab57b9
46 changed files with 2373 additions and 55 deletions

View File

@ -348,8 +348,14 @@ export class WorkflowVersionStepWorkspaceService {
step: WorkflowAction;
workspaceId: string;
}): Promise<WorkflowAction> {
// We don't enrich on the fly for code workflow action. OutputSchema is computed and updated when testing the serverless function
if (step.type === WorkflowActionType.CODE) {
// We don't enrich on the fly for code and HTTP request workflow actions.
// For code actions, OutputSchema is computed and updated when testing the serverless function.
// For HTTP requests, OutputSchema is determined by the expamle response input
if (
[WorkflowActionType.CODE, WorkflowActionType.HTTP_REQUEST].includes(
step.type,
)
) {
return step;
}
@ -555,6 +561,23 @@ export class WorkflowVersionStepWorkspaceService {
},
};
}
case WorkflowActionType.HTTP_REQUEST: {
return {
id: newStepId,
name: 'HTTP Request',
type: WorkflowActionType.HTTP_REQUEST,
valid: false,
settings: {
...BASE_STEP_DEFINITION,
input: {
url: '',
method: 'GET',
headers: {},
body: {},
},
},
};
}
default:
throw new WorkflowVersionStepException(
`WorkflowActionType '${type}' unknown`,

View File

@ -9,6 +9,7 @@ import {
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 { HttpRequestWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/http-request/http-request.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';
@ -27,6 +28,7 @@ export class WorkflowExecutorFactory {
private readonly findRecordsWorkflowAction: FindRecordsWorkflowAction,
private readonly formWorkflowAction: FormWorkflowAction,
private readonly filterWorkflowAction: FilterWorkflowAction,
private readonly httpRequestWorkflowAction: HttpRequestWorkflowAction,
) {}
get(stepType: WorkflowActionType): WorkflowExecutor {
@ -47,6 +49,8 @@ export class WorkflowExecutorFactory {
return this.formWorkflowAction;
case WorkflowActionType.FILTER:
return this.filterWorkflowAction;
case WorkflowActionType.HTTP_REQUEST:
return this.httpRequestWorkflowAction;
default:
throw new WorkflowStepExecutorException(
`Workflow step executor not found for step type '${stepType}'`,

View File

@ -44,16 +44,18 @@ const resolveObject = (
input: object,
context: Record<string, unknown>,
): object => {
const resolvedObject = input;
return Object.entries(input).reduce<Record<string, unknown>>(
(resolvedObject, [key, value]) => {
const resolvedKey = resolveInput(key, context);
const entries = Object.entries(resolvedObject);
resolvedObject[
typeof resolvedKey === 'string' ? resolvedKey : String(resolvedKey)
] = resolveInput(value, context);
for (const [key, value] of entries) {
// @ts-expect-error legacy noImplicitAny
resolvedObject[key] = resolveInput(value, context);
}
return resolvedObject;
return resolvedObject;
},
{},
);
};
const resolveString = (

View File

@ -0,0 +1,11 @@
import {
WorkflowAction,
WorkflowActionType,
WorkflowHttpRequestAction,
} from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
export const isWorkflowHttpRequestAction = (
action: WorkflowAction,
): action is WorkflowHttpRequestAction => {
return action.type === WorkflowActionType.HTTP_REQUEST;
};

View File

@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { HttpRequestWorkflowAction } from './http-request.workflow-action';
@Module({
providers: [HttpRequestWorkflowAction],
exports: [HttpRequestWorkflowAction],
})
export class HttpRequestActionModule {}

View File

@ -0,0 +1,73 @@
import { Injectable } from '@nestjs/common';
import axios, { AxiosRequestConfig } from 'axios';
import { WorkflowExecutor } from 'src/modules/workflow/workflow-executor/interfaces/workflow-executor.interface';
import {
WorkflowStepExecutorException,
WorkflowStepExecutorExceptionCode,
} from 'src/modules/workflow/workflow-executor/exceptions/workflow-step-executor.exception';
import { WorkflowExecutorInput } from 'src/modules/workflow/workflow-executor/types/workflow-executor-input';
import { WorkflowExecutorOutput } from 'src/modules/workflow/workflow-executor/types/workflow-executor-output.type';
import { resolveInput } from 'src/modules/workflow/workflow-executor/utils/variable-resolver.util';
import { isWorkflowHttpRequestAction } from './guards/is-workflow-http-request-action.guard';
import { WorkflowHttpRequestActionInput } from './types/workflow-http-request-action-input.type';
@Injectable()
export class HttpRequestWorkflowAction implements WorkflowExecutor {
async execute({
currentStepId,
steps,
context,
}: WorkflowExecutorInput): Promise<WorkflowExecutorOutput> {
const step = steps.find((step) => step.id === currentStepId);
if (!step) {
throw new WorkflowStepExecutorException(
'Step not found',
WorkflowStepExecutorExceptionCode.STEP_NOT_FOUND,
);
}
if (!isWorkflowHttpRequestAction(step)) {
throw new WorkflowStepExecutorException(
'Step is not an HTTP Request action',
WorkflowStepExecutorExceptionCode.INVALID_STEP_TYPE,
);
}
const workflowActionInput = resolveInput(
step.settings.input,
context,
) as WorkflowHttpRequestActionInput;
const { url, method, headers, body } = workflowActionInput;
try {
const axiosConfig: AxiosRequestConfig = {
url,
method: method,
headers,
};
if (['POST', 'PUT', 'PATCH'].includes(method) && body) {
axiosConfig.data = body;
}
const response = await axios(axiosConfig);
return { result: response.data };
} catch (error) {
if (axios.isAxiosError(error)) {
return {
error: error.response?.data || error.message || 'HTTP request failed',
};
}
return {
error: error instanceof Error ? error.message : 'HTTP request failed',
};
}
}
}

View File

@ -0,0 +1,14 @@
export type WorkflowHttpRequestActionInput = {
url: string;
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
headers?: Record<string, string>;
body?: Record<
string,
| string
| number
| boolean
| null
| undefined
| Array<string | number | boolean | null>
>;
};

View File

@ -0,0 +1,7 @@
import { BaseWorkflowActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action-settings.type';
import { WorkflowHttpRequestActionInput } from './workflow-http-request-action-input.type';
export type WorkflowHttpRequestActionSettings = BaseWorkflowActionSettings & {
input: WorkflowHttpRequestActionInput;
};

View File

@ -2,6 +2,7 @@ import { OutputSchema } from 'src/modules/workflow/workflow-builder/workflow-sch
import { WorkflowCodeActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/code/types/workflow-code-action-settings.type';
import { WorkflowFilterActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/filter/types/workflow-filter-action-settings.type';
import { WorkflowFormActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/form/types/workflow-form-action-settings.type';
import { WorkflowHttpRequestActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/http-request/types/workflow-http-request-action-settings.type';
import { WorkflowSendEmailActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/mail-sender/types/workflow-send-email-action-settings.type';
import {
WorkflowCreateRecordActionSettings,
@ -30,4 +31,5 @@ export type WorkflowActionSettings =
| WorkflowDeleteRecordActionSettings
| WorkflowFindRecordsActionSettings
| WorkflowFormActionSettings
| WorkflowFilterActionSettings;
| WorkflowFilterActionSettings
| WorkflowHttpRequestActionSettings;

View File

@ -1,6 +1,7 @@
import { WorkflowCodeActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/code/types/workflow-code-action-settings.type';
import { WorkflowFilterActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/filter/types/workflow-filter-action-settings.type';
import { WorkflowFormActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/form/types/workflow-form-action-settings.type';
import { WorkflowHttpRequestActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/http-request/types/workflow-http-request-action-settings.type';
import { WorkflowSendEmailActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/mail-sender/types/workflow-send-email-action-settings.type';
import {
WorkflowCreateRecordActionSettings,
@ -19,6 +20,7 @@ export enum WorkflowActionType {
FIND_RECORDS = 'FIND_RECORDS',
FORM = 'FORM',
FILTER = 'FILTER',
HTTP_REQUEST = 'HTTP_REQUEST',
}
type BaseWorkflowAction = {
@ -70,6 +72,11 @@ export type WorkflowFilterAction = BaseWorkflowAction & {
settings: WorkflowFilterActionSettings;
};
export type WorkflowHttpRequestAction = BaseWorkflowAction & {
type: WorkflowActionType.HTTP_REQUEST;
settings: WorkflowHttpRequestActionSettings;
};
export type WorkflowAction =
| WorkflowCodeAction
| WorkflowSendEmailAction
@ -78,4 +85,5 @@ export type WorkflowAction =
| WorkflowDeleteRecordAction
| WorkflowFindRecordsAction
| WorkflowFormAction
| WorkflowFilterAction;
| WorkflowFilterAction
| WorkflowHttpRequestAction;

View File

@ -7,6 +7,7 @@ import { WorkflowExecutorFactory } from 'src/modules/workflow/workflow-executor/
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 { HttpRequestActionModule } from 'src/modules/workflow/workflow-executor/workflow-actions/http-request/http-request-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 { WorkflowExecutorWorkspaceService } from 'src/modules/workflow/workflow-executor/workspace-services/workflow-executor.workspace-service';
@ -22,6 +23,7 @@ import { WorkflowRunModule } from 'src/modules/workflow/workflow-runner/workflow
WorkflowRunModule,
BillingModule,
FilterActionModule,
HttpRequestActionModule,
],
providers: [
WorkflowExecutorWorkspaceService,