diff --git a/packages/twenty-client-sdk/src/metadata/generated/schema.graphql b/packages/twenty-client-sdk/src/metadata/generated/schema.graphql
index 720c883fc9..35549ee0bd 100644
--- a/packages/twenty-client-sdk/src/metadata/generated/schema.graphql
+++ b/packages/twenty-client-sdk/src/metadata/generated/schema.graphql
@@ -2989,6 +2989,11 @@ type WorkspaceAiStats {
toolsCount: Int!
}
+type EnqueueJobResult {
+ enqueued: Boolean!
+ logicFunctionUniversalIdentifier: String!
+}
+
type AppKeyValue {
key: String!
value: JSON
@@ -3543,6 +3548,7 @@ type Mutation {
updateCalendarChannel(input: UpdateCalendarChannelInput!): CalendarChannel!
setAppKeyValue(input: SetAppKeyValueInput!): AppKeyValue!
deleteAppKeyValue(key: String!, scope: AppKeyValueScope = WORKSPACE): Boolean!
+ enqueueJob(input: EnqueueJobInput!): EnqueueJobResult!
createChatThread: AgentChatThread!
sendChatMessage(threadId: UUID!, text: String!, messageId: UUID!, browsingContext: JSON, modelId: String, fileAttachments: [FileAttachmentInput!]): SendChatMessageResult!
retryChatMessage(threadId: UUID!, modelId: String): SendChatMessageResult!
@@ -4691,6 +4697,13 @@ input SetAppKeyValueInput {
scope: AppKeyValueScope = WORKSPACE
}
+input EnqueueJobInput {
+ logicFunctionUniversalIdentifier: String!
+ payload: JSON
+ retryLimit: Int
+ delayMs: Int
+}
+
input FileAttachmentInput {
id: UUID!
filename: String!
diff --git a/packages/twenty-client-sdk/src/metadata/generated/schema.ts b/packages/twenty-client-sdk/src/metadata/generated/schema.ts
index cce8ca9e1f..d9333e73ff 100644
--- a/packages/twenty-client-sdk/src/metadata/generated/schema.ts
+++ b/packages/twenty-client-sdk/src/metadata/generated/schema.ts
@@ -2682,6 +2682,12 @@ export interface WorkspaceAiStats {
__typename: 'WorkspaceAiStats'
}
+export interface EnqueueJobResult {
+ enqueued: Scalars['Boolean']
+ logicFunctionUniversalIdentifier: Scalars['String']
+ __typename: 'EnqueueJobResult'
+}
+
export interface AppKeyValue {
key: Scalars['String']
value?: Scalars['JSON']
@@ -3062,6 +3068,7 @@ export interface Mutation {
updateCalendarChannel: CalendarChannel
setAppKeyValue: AppKeyValue
deleteAppKeyValue: Scalars['Boolean']
+ enqueueJob: EnqueueJobResult
createChatThread: AgentChatThread
sendChatMessage: SendChatMessageResult
retryChatMessage: SendChatMessageResult
@@ -5964,6 +5971,13 @@ export interface WorkspaceAiStatsGenqlSelection{
__scalar?: boolean | number
}
+export interface EnqueueJobResultGenqlSelection{
+ enqueued?: boolean | number
+ logicFunctionUniversalIdentifier?: boolean | number
+ __typename?: boolean | number
+ __scalar?: boolean | number
+}
+
export interface AppKeyValueGenqlSelection{
key?: boolean | number
value?: boolean | number
@@ -6376,6 +6390,7 @@ export interface MutationGenqlSelection{
updateCalendarChannel?: (CalendarChannelGenqlSelection & { __args: {input: UpdateCalendarChannelInput} })
setAppKeyValue?: (AppKeyValueGenqlSelection & { __args: {input: SetAppKeyValueInput} })
deleteAppKeyValue?: { __args: {key: Scalars['String'], scope?: (AppKeyValueScope | null)} }
+ enqueueJob?: (EnqueueJobResultGenqlSelection & { __args: {input: EnqueueJobInput} })
createChatThread?: AgentChatThreadGenqlSelection
sendChatMessage?: (SendChatMessageResultGenqlSelection & { __args: {threadId: Scalars['UUID'], text: Scalars['String'], messageId: Scalars['UUID'], browsingContext?: (Scalars['JSON'] | null), modelId?: (Scalars['String'] | null), fileAttachments?: (FileAttachmentInput[] | null)} })
retryChatMessage?: (SendChatMessageResultGenqlSelection & { __args: {threadId: Scalars['UUID'], modelId?: (Scalars['String'] | null)} })
@@ -6805,6 +6820,8 @@ export interface UpdateCalendarChannelInputUpdates {visibility?: (CalendarChanne
export interface SetAppKeyValueInput {key: Scalars['String'],value?: (Scalars['JSON'] | null),scope?: (AppKeyValueScope | null)}
+export interface EnqueueJobInput {logicFunctionUniversalIdentifier: Scalars['String'],payload?: (Scalars['JSON'] | null),retryLimit?: (Scalars['Int'] | null),delayMs?: (Scalars['Int'] | null)}
+
export interface FileAttachmentInput {id: Scalars['UUID'],filename: Scalars['String']}
export interface AgentChatQuestionAnswerInput {questionIndex: Scalars['Int'],selectedOptionIndices: Scalars['Int'][],freeText?: (Scalars['String'] | null)}
@@ -8891,6 +8908,14 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
+ const EnqueueJobResult_possibleTypes: string[] = ['EnqueueJobResult']
+ export const isEnqueueJobResult = (obj?: { __typename?: any } | null): obj is EnqueueJobResult => {
+ if (!obj?.__typename) throw new Error('__typename is missing in "isEnqueueJobResult"')
+ return EnqueueJobResult_possibleTypes.includes(obj.__typename)
+ }
+
+
+
const AppKeyValue_possibleTypes: string[] = ['AppKeyValue']
export const isAppKeyValue = (obj?: { __typename?: any } | null): obj is AppKeyValue => {
if (!obj?.__typename) throw new Error('__typename is missing in "isAppKeyValue"')
diff --git a/packages/twenty-client-sdk/src/metadata/generated/types.ts b/packages/twenty-client-sdk/src/metadata/generated/types.ts
index 8ae29a3172..74c3681805 100644
--- a/packages/twenty-client-sdk/src/metadata/generated/types.ts
+++ b/packages/twenty-client-sdk/src/metadata/generated/types.ts
@@ -76,18 +76,18 @@ export default {
292,
295,
332,
- 337,
- 339,
+ 338,
340,
341,
342,
- 344,
- 346,
- 359,
- 366,
- 373,
+ 343,
+ 345,
+ 347,
+ 360,
+ 367,
374,
- 505
+ 375,
+ 507
],
"types": {
"BillingProductDTO": {
@@ -6007,6 +6007,17 @@ export default {
1
]
},
+ "EnqueueJobResult": {
+ "enqueued": [
+ 3
+ ],
+ "logicFunctionUniversalIdentifier": [
+ 1
+ ],
+ "__typename": [
+ 1
+ ]
+ },
"AppKeyValue": {
"key": [
1
@@ -6015,7 +6026,7 @@ export default {
5
],
"scope": [
- 337
+ 338
],
"__typename": [
1
@@ -6030,19 +6041,19 @@ export default {
1
],
"syncStatus": [
- 339
- ],
- "syncStage": [
340
],
- "visibility": [
+ "syncStage": [
341
],
+ "visibility": [
+ 342
+ ],
"isContactAutoCreationEnabled": [
3
],
"contactAutoCreationPolicy": [
- 342
+ 343
],
"isSyncEnabled": [
3
@@ -6093,7 +6104,7 @@ export default {
1
],
"pendingSyncAction": [
- 344
+ 345
],
"messageChannelId": [
4
@@ -6111,7 +6122,7 @@ export default {
"MessageFolderPendingSyncAction": {},
"CollectionHash": {
"collectionName": [
- 346
+ 347
],
"hash": [
1
@@ -6175,13 +6186,13 @@ export default {
},
"MinimalMetadata": {
"objectMetadataItems": [
- 347
- ],
- "views": [
348
],
+ "views": [
+ 349
+ ],
"collectionHashes": [
- 345
+ 346
],
"__typename": [
1
@@ -6355,7 +6366,7 @@ export default {
7,
{
"input": [
- 351,
+ 352,
"GetApiKeyInput!"
]
}
@@ -6458,7 +6469,7 @@ export default {
11,
{
"input": [
- 352,
+ 353,
"AgentIdInput!"
]
}
@@ -6492,7 +6503,7 @@ export default {
"CursorPaging!"
],
"filter": [
- 353,
+ 354,
"ObjectFilter!"
]
}
@@ -6501,7 +6512,7 @@ export default {
22,
{
"input": [
- 354,
+ 355,
"LogicFunctionIdInput!"
]
}
@@ -6513,7 +6524,7 @@ export default {
5,
{
"input": [
- 354,
+ 355,
"LogicFunctionIdInput!"
]
}
@@ -6522,7 +6533,7 @@ export default {
1,
{
"input": [
- 354,
+ 355,
"LogicFunctionIdInput!"
]
}
@@ -6744,7 +6755,7 @@ export default {
286,
{
"input": [
- 355,
+ 356,
"PreviewMessageCampaignAudienceInput!"
]
}
@@ -6753,7 +6764,7 @@ export default {
293,
{
"input": [
- 356,
+ 357,
"FindMessageSuppressionsInput!"
]
}
@@ -6800,7 +6811,7 @@ export default {
}
],
"myMessageFolders": [
- 343,
+ 344,
{
"messageChannelId": [
4
@@ -6808,7 +6819,7 @@ export default {
}
],
"myCalendarChannels": [
- 338,
+ 339,
{
"connectedAccountId": [
4
@@ -6816,17 +6827,17 @@ export default {
}
],
"minimalMetadata": [
- 349
+ 350
],
"appKeyValue": [
- 336,
+ 337,
{
"key": [
1,
"String!"
],
"scope": [
- 337
+ 338
]
}
],
@@ -6834,7 +6845,7 @@ export default {
203,
{
"filter": [
- 357
+ 358
]
}
],
@@ -6965,7 +6976,7 @@ export default {
321,
{
"input": [
- 358,
+ 359,
"EventLogQueryInput!"
]
}
@@ -6974,7 +6985,7 @@ export default {
315,
{
"input": [
- 362,
+ 363,
"PieChartDataInput!"
]
}
@@ -6983,7 +6994,7 @@ export default {
313,
{
"input": [
- 363,
+ 364,
"LineChartDataInput!"
]
}
@@ -6992,7 +7003,7 @@ export default {
310,
{
"input": [
- 364,
+ 365,
"BarChartDataInput!"
]
}
@@ -7042,7 +7053,7 @@ export default {
269,
{
"input": [
- 365
+ 366
]
}
],
@@ -7071,10 +7082,10 @@ export default {
},
"ObjectFilter": {
"and": [
- 353
+ 354
],
"or": [
- 353
+ 354
],
"id": [
33
@@ -7159,10 +7170,10 @@ export default {
},
"EventLogQueryInput": {
"table": [
- 359
+ 360
],
"filters": [
- 360
+ 361
],
"first": [
30
@@ -7183,7 +7194,7 @@ export default {
1
],
"dateRange": [
- 361
+ 362
],
"recordId": [
1
@@ -7250,7 +7261,7 @@ export default {
1
],
"operationTypes": [
- 366
+ 367
],
"__typename": [
1
@@ -7262,7 +7273,7 @@ export default {
3,
{
"input": [
- 368,
+ 369,
"AddQuerySubscriptionInput!"
]
}
@@ -7271,7 +7282,7 @@ export default {
3,
{
"input": [
- 369,
+ 370,
"RemoveQueryFromEventStreamInput!"
]
}
@@ -7280,7 +7291,7 @@ export default {
161,
{
"inputs": [
- 370,
+ 371,
"[CreateNavigationMenuItemInput!]!"
]
}
@@ -7289,7 +7300,7 @@ export default {
161,
{
"input": [
- 370,
+ 371,
"CreateNavigationMenuItemInput!"
]
}
@@ -7298,7 +7309,7 @@ export default {
161,
{
"inputs": [
- 371,
+ 372,
"[UpdateOneNavigationMenuItemInput!]!"
]
}
@@ -7307,7 +7318,7 @@ export default {
161,
{
"input": [
- 371,
+ 372,
"UpdateOneNavigationMenuItemInput!"
]
}
@@ -7342,7 +7353,7 @@ export default {
"Float!"
],
"fileFolder": [
- 373,
+ 374,
"FileFolder!"
],
"fieldMetadataId": [
@@ -7381,7 +7392,7 @@ export default {
148,
{
"file": [
- 374,
+ 375,
"Upload!"
]
}
@@ -7390,7 +7401,7 @@ export default {
148,
{
"file": [
- 374,
+ 375,
"Upload!"
]
}
@@ -7399,7 +7410,7 @@ export default {
148,
{
"file": [
- 374,
+ 375,
"Upload!"
]
}
@@ -7408,7 +7419,7 @@ export default {
148,
{
"file": [
- 374,
+ 375,
"Upload!"
]
}
@@ -7417,7 +7428,7 @@ export default {
148,
{
"file": [
- 374,
+ 375,
"Upload!"
]
}
@@ -7426,7 +7437,7 @@ export default {
148,
{
"file": [
- 374,
+ 375,
"Upload!"
],
"fieldMetadataId": [
@@ -7439,7 +7450,7 @@ export default {
148,
{
"file": [
- 374,
+ 375,
"Upload!"
],
"fieldMetadataUniversalIdentifier": [
@@ -7452,7 +7463,7 @@ export default {
56,
{
"input": [
- 375,
+ 376,
"CreateViewFilterGroupInput!"
]
}
@@ -7465,7 +7476,7 @@ export default {
"String!"
],
"input": [
- 376,
+ 377,
"UpdateViewFilterGroupInput!"
]
}
@@ -7492,7 +7503,7 @@ export default {
58,
{
"input": [
- 377,
+ 378,
"CreateViewFilterInput!"
]
}
@@ -7501,7 +7512,7 @@ export default {
58,
{
"input": [
- 378,
+ 379,
"UpdateViewFilterInput!"
]
}
@@ -7510,7 +7521,7 @@ export default {
58,
{
"input": [
- 380,
+ 381,
"DeleteViewFilterInput!"
]
}
@@ -7519,7 +7530,7 @@ export default {
58,
{
"input": [
- 381,
+ 382,
"DestroyViewFilterInput!"
]
}
@@ -7528,7 +7539,7 @@ export default {
64,
{
"input": [
- 382,
+ 383,
"CreateViewInput!"
]
}
@@ -7541,7 +7552,7 @@ export default {
"String!"
],
"input": [
- 383,
+ 384,
"UpdateViewInput!"
]
}
@@ -7568,7 +7579,7 @@ export default {
64,
{
"input": [
- 384,
+ 385,
"UpsertViewWidgetInput!"
]
}
@@ -7577,7 +7588,7 @@ export default {
61,
{
"input": [
- 390,
+ 391,
"CreateViewSortInput!"
]
}
@@ -7586,7 +7597,7 @@ export default {
61,
{
"input": [
- 391,
+ 392,
"UpdateViewSortInput!"
]
}
@@ -7595,7 +7606,7 @@ export default {
3,
{
"input": [
- 393,
+ 394,
"DeleteViewSortInput!"
]
}
@@ -7604,7 +7615,7 @@ export default {
3,
{
"input": [
- 394,
+ 395,
"DestroyViewSortInput!"
]
}
@@ -7613,7 +7624,7 @@ export default {
54,
{
"input": [
- 395,
+ 396,
"UpdateViewFieldInput!"
]
}
@@ -7622,7 +7633,7 @@ export default {
54,
{
"input": [
- 397,
+ 398,
"CreateViewFieldInput!"
]
}
@@ -7631,7 +7642,7 @@ export default {
54,
{
"inputs": [
- 397,
+ 398,
"[CreateViewFieldInput!]!"
]
}
@@ -7640,7 +7651,7 @@ export default {
54,
{
"input": [
- 398,
+ 399,
"DeleteViewFieldInput!"
]
}
@@ -7649,7 +7660,7 @@ export default {
54,
{
"input": [
- 399,
+ 400,
"DestroyViewFieldInput!"
]
}
@@ -7658,7 +7669,7 @@ export default {
63,
{
"input": [
- 400,
+ 401,
"UpdateViewFieldGroupInput!"
]
}
@@ -7667,7 +7678,7 @@ export default {
63,
{
"input": [
- 402,
+ 403,
"CreateViewFieldGroupInput!"
]
}
@@ -7676,7 +7687,7 @@ export default {
63,
{
"inputs": [
- 402,
+ 403,
"[CreateViewFieldGroupInput!]!"
]
}
@@ -7685,7 +7696,7 @@ export default {
63,
{
"input": [
- 403,
+ 404,
"DeleteViewFieldGroupInput!"
]
}
@@ -7694,7 +7705,7 @@ export default {
63,
{
"input": [
- 404,
+ 405,
"DestroyViewFieldGroupInput!"
]
}
@@ -7703,7 +7714,7 @@ export default {
64,
{
"input": [
- 405,
+ 406,
"UpsertFieldsWidgetInput!"
]
}
@@ -7712,7 +7723,7 @@ export default {
7,
{
"input": [
- 408,
+ 409,
"CreateApiKeyInput!"
]
}
@@ -7721,7 +7732,7 @@ export default {
7,
{
"input": [
- 409,
+ 410,
"UpdateApiKeyInput!"
]
}
@@ -7730,7 +7741,7 @@ export default {
7,
{
"input": [
- 410,
+ 411,
"RevokeApiKeyInput!"
]
}
@@ -7885,7 +7896,7 @@ export default {
147,
{
"input": [
- 411,
+ 412,
"CreateApprovedAccessDomainInput!"
]
}
@@ -7894,7 +7905,7 @@ export default {
3,
{
"input": [
- 412,
+ 413,
"DeleteApprovedAccessDomainInput!"
]
}
@@ -7903,7 +7914,7 @@ export default {
147,
{
"input": [
- 413,
+ 414,
"ValidateApprovedAccessDomainInput!"
]
}
@@ -7912,7 +7923,7 @@ export default {
121,
{
"input": [
- 414,
+ 415,
"CreatePageLayoutTabInput!"
]
}
@@ -7925,7 +7936,7 @@ export default {
"String!"
],
"input": [
- 415,
+ 416,
"UpdatePageLayoutTabInput!"
]
}
@@ -7943,7 +7954,7 @@ export default {
122,
{
"input": [
- 416,
+ 417,
"CreatePageLayoutInput!"
]
}
@@ -7956,7 +7967,7 @@ export default {
"String!"
],
"input": [
- 417,
+ 418,
"UpdatePageLayoutInput!"
]
}
@@ -7978,7 +7989,7 @@ export default {
"String!"
],
"input": [
- 418,
+ 419,
"UpdatePageLayoutWithTabsInput!"
]
}
@@ -8014,7 +8025,7 @@ export default {
81,
{
"input": [
- 422,
+ 423,
"CreatePageLayoutWidgetInput!"
]
}
@@ -8027,7 +8038,7 @@ export default {
"String!"
],
"input": [
- 423,
+ 424,
"UpdatePageLayoutWidgetInput!"
]
}
@@ -8045,7 +8056,7 @@ export default {
11,
{
"input": [
- 424,
+ 425,
"CreateAgentInput!"
]
}
@@ -8054,7 +8065,7 @@ export default {
11,
{
"input": [
- 425,
+ 426,
"UpdateAgentInput!"
]
}
@@ -8063,7 +8074,7 @@ export default {
11,
{
"input": [
- 352,
+ 353,
"AgentIdInput!"
]
}
@@ -8072,7 +8083,7 @@ export default {
28,
{
"input": [
- 426,
+ 427,
"CreateOneObjectInput!"
]
}
@@ -8081,7 +8092,7 @@ export default {
28,
{
"input": [
- 428,
+ 429,
"DeleteOneObjectInput!"
]
}
@@ -8090,7 +8101,7 @@ export default {
28,
{
"input": [
- 429,
+ 430,
"UpdateOneObjectInput!"
]
}
@@ -8099,7 +8110,7 @@ export default {
26,
{
"input": [
- 431,
+ 432,
"CreateOneIndexInput!"
]
}
@@ -8108,7 +8119,7 @@ export default {
26,
{
"input": [
- 434,
+ 435,
"DeleteOneIndexInput!"
]
}
@@ -8117,7 +8128,7 @@ export default {
22,
{
"input": [
- 354,
+ 355,
"LogicFunctionIdInput!"
]
}
@@ -8126,7 +8137,7 @@ export default {
22,
{
"input": [
- 435,
+ 436,
"CreateLogicFunctionFromSourceInput!"
]
}
@@ -8135,7 +8146,7 @@ export default {
143,
{
"input": [
- 436,
+ 437,
"ExecuteOneLogicFunctionInput!"
]
}
@@ -8144,7 +8155,7 @@ export default {
3,
{
"input": [
- 437,
+ 438,
"UpdateLogicFunctionFromSourceInput!"
]
}
@@ -8153,7 +8164,7 @@ export default {
15,
{
"input": [
- 439,
+ 440,
"CreateCommandMenuItemInput!"
]
}
@@ -8162,7 +8173,7 @@ export default {
15,
{
"input": [
- 440,
+ 441,
"UpdateCommandMenuItemInput!"
]
}
@@ -8189,7 +8200,7 @@ export default {
14,
{
"input": [
- 441,
+ 442,
"CreateFrontComponentInput!"
]
}
@@ -8198,7 +8209,7 @@ export default {
14,
{
"input": [
- 442,
+ 443,
"UpdateFrontComponentInput!"
]
}
@@ -8216,7 +8227,7 @@ export default {
70,
{
"data": [
- 444,
+ 445,
"ActivateWorkspaceInput!"
]
}
@@ -8225,7 +8236,7 @@ export default {
70,
{
"data": [
- 445,
+ 446,
"UpdateWorkspaceInput!"
]
}
@@ -8256,7 +8267,7 @@ export default {
200,
{
"input": [
- 446,
+ 447,
"CreateApplicationRegistrationInput!"
]
}
@@ -8265,7 +8276,7 @@ export default {
76,
{
"input": [
- 447,
+ 448,
"UpdateApplicationRegistrationInput!"
]
}
@@ -8292,7 +8303,7 @@ export default {
2,
{
"input": [
- 449,
+ 450,
"CreateApplicationRegistrationVariableInput!"
]
}
@@ -8301,7 +8312,7 @@ export default {
2,
{
"input": [
- 450,
+ 451,
"UpdateApplicationRegistrationVariableInput!"
]
}
@@ -8319,7 +8330,7 @@ export default {
76,
{
"file": [
- 374,
+ 375,
"Upload!"
],
"universalIdentifier": [
@@ -8381,7 +8392,7 @@ export default {
"UUID!"
],
"input": [
- 452,
+ 453,
"UpdateApplicationInput!"
]
}
@@ -8402,7 +8413,7 @@ export default {
24,
{
"input": [
- 453,
+ 454,
"CreateOneFieldMetadataInput!"
]
}
@@ -8411,7 +8422,7 @@ export default {
24,
{
"input": [
- 455,
+ 456,
"UpdateOneFieldMetadataInput!"
]
}
@@ -8420,7 +8431,7 @@ export default {
24,
{
"input": [
- 457,
+ 458,
"DeleteOneFieldInput!"
]
}
@@ -8429,7 +8440,7 @@ export default {
60,
{
"input": [
- 458,
+ 459,
"CreateViewGroupInput!"
]
}
@@ -8438,7 +8449,7 @@ export default {
60,
{
"inputs": [
- 458,
+ 459,
"[CreateViewGroupInput!]!"
]
}
@@ -8447,7 +8458,7 @@ export default {
60,
{
"input": [
- 459,
+ 460,
"UpdateViewGroupInput!"
]
}
@@ -8456,7 +8467,7 @@ export default {
60,
{
"inputs": [
- 459,
+ 460,
"[UpdateViewGroupInput!]!"
]
}
@@ -8465,7 +8476,7 @@ export default {
60,
{
"input": [
- 461,
+ 462,
"DeleteViewGroupInput!"
]
}
@@ -8474,7 +8485,7 @@ export default {
60,
{
"input": [
- 462,
+ 463,
"DestroyViewGroupInput!"
]
}
@@ -8496,7 +8507,7 @@ export default {
49,
{
"createRoleInput": [
- 463,
+ 464,
"CreateRoleInput!"
]
}
@@ -8505,7 +8516,7 @@ export default {
49,
{
"updateRoleInput": [
- 464,
+ 465,
"UpdateRoleInput!"
]
}
@@ -8523,7 +8534,7 @@ export default {
46,
{
"upsertObjectPermissionsInput": [
- 466,
+ 467,
"UpsertObjectPermissionsInput!"
]
}
@@ -8532,7 +8543,7 @@ export default {
47,
{
"upsertPermissionFlagsInput": [
- 468,
+ 469,
"UpsertPermissionFlagsInput!"
]
}
@@ -8541,7 +8552,7 @@ export default {
41,
{
"upsertFieldPermissionsInput": [
- 469,
+ 470,
"UpsertFieldPermissionsInput!"
]
}
@@ -8550,7 +8561,7 @@ export default {
239,
{
"input": [
- 471,
+ 472,
"UpsertRowLevelPermissionPredicatesInput!"
]
}
@@ -8581,7 +8592,7 @@ export default {
287,
{
"input": [
- 474,
+ 475,
"SendEmailViaDomainInput!"
]
}
@@ -8590,7 +8601,7 @@ export default {
289,
{
"input": [
- 475,
+ 476,
"SendMessageCampaignInput!"
]
}
@@ -8599,7 +8610,7 @@ export default {
287,
{
"input": [
- 476,
+ 477,
"SendMessageCampaignTestInput!"
]
}
@@ -8608,7 +8619,7 @@ export default {
294,
{
"input": [
- 477,
+ 478,
"CreateUnsubscribeTopicInput!"
]
}
@@ -8617,7 +8628,7 @@ export default {
294,
{
"input": [
- 478,
+ 479,
"UpdateUnsubscribeTopicInput!"
]
}
@@ -8635,7 +8646,7 @@ export default {
277,
{
"input": [
- 479,
+ 480,
"UpdateMessageChannelInput!"
]
}
@@ -8644,7 +8655,7 @@ export default {
285,
{
"input": [
- 481,
+ 482,
"CreateEmailGroupChannelInput!"
]
}
@@ -8653,7 +8664,7 @@ export default {
277,
{
"input": [
- 482,
+ 483,
"UpdateEmailGroupChannelInput!"
]
}
@@ -8671,7 +8682,7 @@ export default {
275,
{
"input": [
- 483,
+ 484,
"CreateEmailingDomainInput!"
]
}
@@ -8707,7 +8718,7 @@ export default {
306,
{
"input": [
- 484,
+ 485,
"RunAgentInput!"
]
}
@@ -8716,7 +8727,7 @@ export default {
303,
{
"input": [
- 485,
+ 486,
"CreateWebhookInput!"
]
}
@@ -8725,7 +8736,7 @@ export default {
303,
{
"input": [
- 486,
+ 487,
"UpdateWebhookInput!"
]
}
@@ -8740,37 +8751,37 @@ export default {
}
],
"updateMessageFolder": [
- 343,
+ 344,
{
"input": [
- 488,
+ 489,
"UpdateMessageFolderInput!"
]
}
],
"updateMessageFolders": [
- 343,
+ 344,
{
"input": [
- 490,
+ 491,
"UpdateMessageFoldersInput!"
]
}
],
"updateCalendarChannel": [
- 338,
+ 339,
{
"input": [
- 491,
+ 492,
"UpdateCalendarChannelInput!"
]
}
],
"setAppKeyValue": [
- 336,
+ 337,
{
"input": [
- 493,
+ 494,
"SetAppKeyValueInput!"
]
}
@@ -8783,7 +8794,16 @@ export default {
"String!"
],
"scope": [
- 337
+ 338
+ ]
+ }
+ ],
+ "enqueueJob": [
+ 336,
+ {
+ "input": [
+ 495,
+ "EnqueueJobInput!"
]
}
],
@@ -8812,7 +8832,7 @@ export default {
1
],
"fileAttachments": [
- 494,
+ 496,
"[FileAttachmentInput!]"
]
}
@@ -8841,7 +8861,7 @@ export default {
"UUID!"
],
"answers": [
- 495,
+ 497,
"[AgentChatQuestionAnswerInput!]!"
],
"modelId": [
@@ -8919,7 +8939,7 @@ export default {
322,
{
"input": [
- 496,
+ 498,
"CreateSkillInput!"
]
}
@@ -8928,7 +8948,7 @@ export default {
322,
{
"input": [
- 497,
+ 499,
"UpdateSkillInput!"
]
}
@@ -8986,7 +9006,7 @@ export default {
252,
{
"input": [
- 498,
+ 500,
"GetAuthorizationUrlForSSOInput!"
]
}
@@ -9152,7 +9172,7 @@ export default {
255,
{
"input": [
- 499
+ 501
]
}
],
@@ -9164,7 +9184,7 @@ export default {
"String!"
],
"file": [
- 374,
+ 375,
"Upload!"
]
}
@@ -9316,7 +9336,7 @@ export default {
3,
{
"input": [
- 500,
+ 502,
"UpdateWorkspaceMemberSettingsInput!"
]
}
@@ -9350,7 +9370,7 @@ export default {
210,
{
"input": [
- 501,
+ 503,
"SetupOIDCSsoInput!"
]
}
@@ -9359,7 +9379,7 @@ export default {
210,
{
"input": [
- 502,
+ 504,
"SetupSAMLSsoInput!"
]
}
@@ -9368,7 +9388,7 @@ export default {
206,
{
"input": [
- 503,
+ 505,
"DeleteSsoInput!"
]
}
@@ -9377,7 +9397,7 @@ export default {
207,
{
"input": [
- 504,
+ 506,
"EditSsoInput!"
]
}
@@ -9406,7 +9426,7 @@ export default {
318,
{
"type": [
- 505,
+ 507,
"AnalyticsType!"
],
"name": [
@@ -9446,7 +9466,7 @@ export default {
308,
{
"input": [
- 506,
+ 508,
"CreateCalendarEventInput!"
]
}
@@ -9455,7 +9475,7 @@ export default {
317,
{
"input": [
- 507,
+ 509,
"SendEmailInput!"
]
}
@@ -9477,7 +9497,7 @@ export default {
"String!"
],
"connectionParameters": [
- 509,
+ 511,
"EmailAccountConnectionParameters!"
],
"id": [
@@ -9489,7 +9509,7 @@ export default {
170,
{
"input": [
- 511,
+ 513,
"UpdateLabPublicFeatureFlagInput!"
]
}
@@ -9554,7 +9574,7 @@ export default {
272,
{
"file": [
- 374,
+ 375,
"Upload!"
],
"applicationUniversalIdentifier": [
@@ -9562,7 +9582,7 @@ export default {
"String!"
],
"fileFolder": [
- 373,
+ 374,
"FileFolder!"
],
"filePath": [
@@ -9667,7 +9687,7 @@ export default {
4
],
"update": [
- 372
+ 373
],
"__typename": [
1
@@ -9778,7 +9798,7 @@ export default {
4
],
"update": [
- 379
+ 380
],
"__typename": [
1
@@ -9949,20 +9969,20 @@ export default {
4
],
"view": [
- 385
- ],
- "viewFields": [
386
],
- "viewFilters": [
+ "viewFields": [
387
],
- "viewFilterGroups": [
+ "viewFilters": [
388
],
- "viewSorts": [
+ "viewFilterGroups": [
389
],
+ "viewSorts": [
+ 390
+ ],
"__typename": [
1
]
@@ -10110,7 +10130,7 @@ export default {
4
],
"update": [
- 392
+ 393
],
"__typename": [
1
@@ -10148,7 +10168,7 @@ export default {
4
],
"update": [
- 396
+ 397
],
"__typename": [
1
@@ -10224,7 +10244,7 @@ export default {
4
],
"update": [
- 401
+ 402
],
"__typename": [
1
@@ -10288,10 +10308,10 @@ export default {
4
],
"groups": [
- 406
+ 407
],
"fields": [
- 407
+ 408
],
"__typename": [
1
@@ -10311,7 +10331,7 @@ export default {
3
],
"fields": [
- 407
+ 408
],
"__typename": [
1
@@ -10479,7 +10499,7 @@ export default {
4
],
"tabs": [
- 419
+ 420
],
"__typename": [
1
@@ -10502,7 +10522,7 @@ export default {
85
],
"widgets": [
- 420
+ 421
],
"__typename": [
1
@@ -10525,7 +10545,7 @@ export default {
4
],
"gridPosition": [
- 421
+ 422
],
"position": [
5
@@ -10574,7 +10594,7 @@ export default {
4
],
"gridPosition": [
- 421
+ 422
],
"position": [
5
@@ -10600,7 +10620,7 @@ export default {
4
],
"gridPosition": [
- 421
+ 422
],
"position": [
5
@@ -10693,7 +10713,7 @@ export default {
},
"CreateOneObjectInput": {
"object": [
- 427
+ 428
],
"__typename": [
1
@@ -10753,7 +10773,7 @@ export default {
},
"UpdateOneObjectInput": {
"update": [
- 430
+ 431
],
"id": [
4
@@ -10808,7 +10828,7 @@ export default {
},
"CreateOneIndexInput": {
"index": [
- 432
+ 433
],
"__typename": [
1
@@ -10819,7 +10839,7 @@ export default {
4
],
"fields": [
- 433
+ 434
],
"indexType": [
27
@@ -10904,7 +10924,7 @@ export default {
4
],
"update": [
- 438
+ 439
],
"__typename": [
1
@@ -11064,7 +11084,7 @@ export default {
4
],
"update": [
- 443
+ 444
],
"__typename": [
1
@@ -11191,7 +11211,7 @@ export default {
1
],
"update": [
- 448
+ 449
],
"__typename": [
1
@@ -11245,7 +11265,7 @@ export default {
1
],
"update": [
- 451
+ 452
],
"__typename": [
1
@@ -11275,7 +11295,7 @@ export default {
},
"CreateOneFieldMetadataInput": {
"field": [
- 454
+ 455
],
"__typename": [
1
@@ -11348,7 +11368,7 @@ export default {
4
],
"update": [
- 456
+ 457
],
"__typename": [
1
@@ -11443,7 +11463,7 @@ export default {
4
],
"update": [
- 460
+ 461
],
"__typename": [
1
@@ -11528,7 +11548,7 @@ export default {
},
"UpdateRoleInput": {
"update": [
- 465
+ 466
],
"id": [
4
@@ -11583,7 +11603,7 @@ export default {
4
],
"objectPermissions": [
- 467
+ 468
],
"__typename": [
1
@@ -11625,7 +11645,7 @@ export default {
4
],
"fieldPermissions": [
- 470
+ 471
],
"__typename": [
1
@@ -11656,10 +11676,10 @@ export default {
4
],
"predicates": [
- 472
+ 473
],
"predicateGroups": [
- 473
+ 474
],
"__typename": [
1
@@ -11813,7 +11833,7 @@ export default {
4
],
"update": [
- 480
+ 481
],
"__typename": [
1
@@ -11911,7 +11931,7 @@ export default {
4
],
"update": [
- 487
+ 488
],
"__typename": [
1
@@ -11939,7 +11959,7 @@ export default {
4
],
"update": [
- 489
+ 490
],
"__typename": [
1
@@ -11958,7 +11978,7 @@ export default {
4
],
"update": [
- 489
+ 490
],
"__typename": [
1
@@ -11969,7 +11989,7 @@ export default {
4
],
"update": [
- 492
+ 493
],
"__typename": [
1
@@ -11977,13 +11997,13 @@ export default {
},
"UpdateCalendarChannelInputUpdates": {
"visibility": [
- 341
+ 342
],
"isContactAutoCreationEnabled": [
3
],
"contactAutoCreationPolicy": [
- 342
+ 343
],
"isSyncEnabled": [
3
@@ -12000,7 +12020,24 @@ export default {
5
],
"scope": [
- 337
+ 338
+ ],
+ "__typename": [
+ 1
+ ]
+ },
+ "EnqueueJobInput": {
+ "logicFunctionUniversalIdentifier": [
+ 1
+ ],
+ "payload": [
+ 5
+ ],
+ "retryLimit": [
+ 30
+ ],
+ "delayMs": [
+ 30
],
"__typename": [
1
@@ -12237,7 +12274,7 @@ export default {
1
],
"files": [
- 508
+ 510
],
"__typename": [
1
@@ -12259,13 +12296,13 @@ export default {
1
],
"IMAP": [
- 510
+ 512
],
"SMTP": [
- 510
+ 512
],
"CALDAV": [
- 510
+ 512
],
"__typename": [
1
@@ -12316,7 +12353,7 @@ export default {
240,
{
"input": [
- 513,
+ 515,
"LogicFunctionLogsInput!"
]
}
@@ -12334,7 +12371,7 @@ export default {
319,
{
"table": [
- 359,
+ 360,
"EventLogTable!"
]
}
diff --git a/packages/twenty-docs/developers/extend/apps/logic/background-jobs.mdx b/packages/twenty-docs/developers/extend/apps/logic/background-jobs.mdx
new file mode 100644
index 0000000000..f5b74c8d5c
--- /dev/null
+++ b/packages/twenty-docs/developers/extend/apps/logic/background-jobs.mdx
@@ -0,0 +1,165 @@
+---
+title: Background Jobs
+description: Hand long or rate-limited work to the Twenty workers by enqueuing another logic function run instead of doing everything inline.
+icon: "layer-group"
+---
+
+A logic function run is capped by its `timeoutSeconds` (900 seconds maximum). Anything that can't finish in that window — a full re-sync, a per-record fan-out, a third-party API that rate-limits you — has to be split into smaller runs.
+
+`enqueueJob` does exactly that: it asks the Twenty workers to run one of your app's logic functions later, in its own process, with its own timeout budget. The caller returns immediately.
+
+```text
+ ┌─────────────────┐ enqueueJob(...) ┌──────────────┐ ┌────────────────────┐
+ │ Logic function │ ─────────────────▶ │ Job queue │──▶│ Logic function │
+ │ (returns now) │ │ (workers) │ │ (fresh run/timeout)│
+ └─────────────────┘ └──────────────┘ └────────────────────┘
+```
+
+## Enqueue a run
+
+Import `enqueueJob` from `twenty-sdk/logic-function` and point it at the `universalIdentifier` of the logic function you want to run.
+
+```ts src/logic-functions/sync-all-contacts.ts
+import { enqueueJob } from 'twenty-sdk/logic-function';
+
+await enqueueJob({
+ logicFunctionUniversalIdentifier: '9f1c3d7e-51b8-4a29-8f0d-7c4e2a6b1d33',
+ payload: { page: 1 },
+});
+```
+
+The target function receives `payload` as its handler argument, exactly like any other trigger. It must belong to the **same application** as the caller — enqueuing another app's function is rejected with `Logic function not found`.
+
+
+`enqueueJob` returns as soon as the job is accepted, not when it has run. It does not return the target's result — have the target write what it produces to the [key-value store](/developers/extend/apps/logic/key-value-store) or to a workspace record if you need to read it back.
+
+
+## Job options
+
+| Option | Default | Range | What it does |
+|--------|---------|-------|--------------|
+| `retryLimit` | `0` | `0`–`10` | Extra attempts if the run throws. Only raise this for handlers that are safe to run twice. |
+| `delayMs` | `0` | `0`–`604800000` (7 days) | Wait this long before the run becomes eligible. |
+
+```ts
+await enqueueJob({
+ logicFunctionUniversalIdentifier: '9f1c3d7e-51b8-4a29-8f0d-7c4e2a6b1d33',
+ payload: { page: 1 },
+ retryLimit: 3,
+ delayMs: 60_000,
+});
+```
+
+
+**Priority is not configurable yet.** Enqueued jobs always run at the lowest priority, so platform work is never delayed behind application jobs. Control over priority is coming soon.
+
+
+The queued run inherits the acting user of the function that enqueued it, so it acts with the same permissions.
+
+## Use it: page through a long sync
+
+The classic shape is a function that enqueues *itself* with the next cursor. Each run does one page of work well inside its own timeout, and the chain stops when there is nothing left.
+
+```ts src/logic-functions/sync-contacts-page.ts
+import { defineLogicFunction } from 'twenty-sdk/define';
+import { enqueueJob } from 'twenty-sdk/logic-function';
+
+const SYNC_CONTACTS_PAGE = '9f1c3d7e-51b8-4a29-8f0d-7c4e2a6b1d33';
+
+const handler = async (params: { cursor?: string }) => {
+ const { contacts, nextCursor } = await fetchContactsPage(params.cursor);
+
+ await importContacts(contacts);
+
+ if (nextCursor) {
+ await enqueueJob({
+ logicFunctionUniversalIdentifier: SYNC_CONTACTS_PAGE,
+ payload: { cursor: nextCursor },
+ delayMs: 2_000,
+ });
+ }
+
+ return { imported: contacts.length, done: !nextCursor };
+};
+
+export default defineLogicFunction({
+ universalIdentifier: SYNC_CONTACTS_PAGE,
+ name: 'sync-contacts-page',
+ timeoutSeconds: 120,
+ handler,
+});
+```
+
+## Fan out per record
+
+When the work is naturally per-item, enqueue one job per item and let the workers process them in parallel instead of looping inline.
+
+```ts
+const companies = await listCompaniesToEnrich();
+
+await Promise.all(
+ companies.map((company) =>
+ enqueueJob({
+ logicFunctionUniversalIdentifier: ENRICH_COMPANY,
+ payload: { companyId: company.id },
+ retryLimit: 2,
+ }),
+ ),
+);
+```
+
+## Good practice for long-running work
+
+Two rules cover almost every long job: **recurse instead of looping**, and **process a bounded chunk per run**.
+
+A run that tries to do everything is the failure mode — it hits the timeout, and with a retry it starts the whole thing again from zero. Instead, size one chunk so it comfortably finishes inside `timeoutSeconds`, persist your position, and enqueue the next run.
+
+```ts src/logic-functions/enrich-companies-batch.ts
+import { defineLogicFunction } from 'twenty-sdk/define';
+import { enqueueJob, kv } from 'twenty-sdk/logic-function';
+
+const ENRICH_COMPANIES_BATCH = '3f9d1c02-8a44-4f0e-b1d7-9c2e5a7b4f10';
+const CHUNK_SIZE = 50;
+
+const handler = async (params: { offset?: number }) => {
+ const offset = params.offset ?? 0;
+ const companies = await listCompaniesToEnrich({
+ offset,
+ limit: CHUNK_SIZE,
+ });
+
+ for (const company of companies) {
+ await enrichCompany(company);
+ }
+
+ await kv.set('enrich:progress', { offset: offset + companies.length });
+
+ if (companies.length === CHUNK_SIZE) {
+ await enqueueJob({
+ logicFunctionUniversalIdentifier: ENRICH_COMPANIES_BATCH,
+ payload: { offset: offset + CHUNK_SIZE },
+ });
+ }
+
+ return { processed: companies.length, done: companies.length < CHUNK_SIZE };
+};
+
+export default defineLogicFunction({
+ universalIdentifier: ENRICH_COMPANIES_BATCH,
+ name: 'enrich-companies-batch',
+ timeoutSeconds: 300,
+ handler,
+});
+```
+
+What makes this hold up:
+
+- **Size the chunk from the slowest item, not the average.** `CHUNK_SIZE × worst-case item time` has to fit in `timeoutSeconds` with room to spare, or the tail of a chunk is lost when the run is cut off.
+- **Make the terminating condition explicit.** Recurse only while a full chunk came back. A chain that stops on "no results" alone will keep going forever if the source ever returns a short page mid-way.
+- **Persist progress before enqueuing the next run,** so a failed link restarts from the last completed chunk instead of the beginning.
+- **Keep each chunk idempotent.** Reprocessing one chunk after a retry must not double-write — key writes on the record or external id you are processing.
+- **Prefer a chunked chain over one giant fan-out** when the work hits a rate-limited third party: a chain with `delayMs` paces itself, whereas thousands of jobs enqueued at once all become eligible immediately.
+
+
+Retries re-run the whole handler. Keep enqueued handlers idempotent before setting `retryLimit` above `0`.
+
diff --git a/packages/twenty-docs/developers/extend/apps/logic/overview.mdx b/packages/twenty-docs/developers/extend/apps/logic/overview.mdx
index f4ac0a1b20..2f7d841a64 100644
--- a/packages/twenty-docs/developers/extend/apps/logic/overview.mdx
+++ b/packages/twenty-docs/developers/extend/apps/logic/overview.mdx
@@ -37,6 +37,9 @@ A Twenty app's **logic layer** is the code that *runs* — server-side TypeScrip
Persist state between logic function runs — caches, cursors, and cross-workspace claims.
+
+ Enqueue a logic function run on the workers to get past the per-run timeout.
+
## Trigger types at a glance
diff --git a/packages/twenty-docs/docs.json b/packages/twenty-docs/docs.json
index d1b33d05e4..749d9216d2 100644
--- a/packages/twenty-docs/docs.json
+++ b/packages/twenty-docs/docs.json
@@ -436,6 +436,7 @@
"developers/extend/apps/logic/overview",
"developers/extend/apps/logic/logic-functions",
"developers/extend/apps/logic/key-value-store",
+ "developers/extend/apps/logic/background-jobs",
"developers/extend/apps/logic/skills-and-agents",
"developers/extend/apps/logic/connections"
]
diff --git a/packages/twenty-docs/navigation/base-structure.json b/packages/twenty-docs/navigation/base-structure.json
index 22b826962c..bb6aa46d6c 100644
--- a/packages/twenty-docs/navigation/base-structure.json
+++ b/packages/twenty-docs/navigation/base-structure.json
@@ -437,6 +437,7 @@
"developers/extend/apps/logic/overview",
"developers/extend/apps/logic/logic-functions",
"developers/extend/apps/logic/key-value-store",
+ "developers/extend/apps/logic/background-jobs",
"developers/extend/apps/logic/skills-and-agents",
"developers/extend/apps/logic/connections"
]
diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts
index 894def3e48..d3e795cf45 100644
--- a/packages/twenty-front/src/generated-metadata/graphql.ts
+++ b/packages/twenty-front/src/generated-metadata/graphql.ts
@@ -1680,6 +1680,19 @@ export enum EngineComponentKey {
VIEW_PREVIOUS_AI_CHATS = 'VIEW_PREVIOUS_AI_CHATS'
}
+export type EnqueueJobInput = {
+ delayMs?: InputMaybe;
+ logicFunctionUniversalIdentifier: Scalars['String']['input'];
+ payload?: InputMaybe;
+ retryLimit?: InputMaybe;
+};
+
+export type EnqueueJobResult = {
+ __typename?: 'EnqueueJobResult';
+ enqueued: Scalars['Boolean']['output'];
+ logicFunctionUniversalIdentifier: Scalars['String']['output'];
+};
+
export type EnterpriseLicenseInfoDto = {
__typename?: 'EnterpriseLicenseInfoDTO';
expiresAt?: Maybe;
@@ -2673,6 +2686,7 @@ export type Mutation = {
editSSOIdentityProvider: EditSso;
emailPasswordResetLink: EmailPasswordResetLink;
endSubscriptionTrialPeriod: BillingEndTrialPeriod;
+ enqueueJob: EnqueueJobResult;
enrichWorkspaceCompany: WorkspaceCompanyEnrichmentResult;
evaluateAgentTurn: AgentTurnEvaluation;
executeOneLogicFunction: LogicFunctionExecutionResult;
@@ -3328,6 +3342,11 @@ export type MutationEmailPasswordResetLinkArgs = {
};
+export type MutationEnqueueJobArgs = {
+ input: EnqueueJobInput;
+};
+
+
export type MutationEvaluateAgentTurnArgs = {
turnId: Scalars['UUID']['input'];
};
diff --git a/packages/twenty-sdk/src/sdk/logic-function/index.ts b/packages/twenty-sdk/src/sdk/logic-function/index.ts
index 690891810d..70dc883d54 100644
--- a/packages/twenty-sdk/src/sdk/logic-function/index.ts
+++ b/packages/twenty-sdk/src/sdk/logic-function/index.ts
@@ -58,6 +58,13 @@ export type { AppConnection } from '@/sdk/logic-function/connections/types/app-c
export { runAgent } from '@/sdk/logic-function/agents/run-agent';
export type { RunAgentInput, RunAgentResult } from 'twenty-shared/application';
+export { enqueueJob } from '@/sdk/logic-function/jobs/enqueue-job';
+export type {
+ EnqueueJobInput,
+ EnqueueJobOptions,
+ EnqueueJobResult,
+} from 'twenty-shared/application';
+
export { kv } from '@/sdk/logic-function/key-value/kv';
export type { AppKeyValue, AppKeyValueScope } from 'twenty-shared/application';
diff --git a/packages/twenty-sdk/src/sdk/logic-function/jobs/__tests__/enqueue-job.spec.ts b/packages/twenty-sdk/src/sdk/logic-function/jobs/__tests__/enqueue-job.spec.ts
new file mode 100644
index 0000000000..3baf2842e2
--- /dev/null
+++ b/packages/twenty-sdk/src/sdk/logic-function/jobs/__tests__/enqueue-job.spec.ts
@@ -0,0 +1,116 @@
+import {
+ afterEach,
+ beforeEach,
+ describe,
+ expect,
+ it,
+ vi,
+ type MockInstance,
+} from 'vitest';
+
+const TARGET_UNIVERSAL_IDENTIFIER = '5a2f4d2a-1a1e-4c66-8a54-1f0a2b3c4d5e';
+
+const importEnqueueJob = async () => {
+ const module = await import('@/sdk/logic-function/jobs/enqueue-job');
+
+ return module.enqueueJob;
+};
+
+const graphqlResponse = (data: unknown) =>
+ new Response(JSON.stringify({ data }), {
+ status: 200,
+ headers: { 'Content-Type': 'application/json' },
+ });
+
+const successResponse = () =>
+ graphqlResponse({
+ enqueueJob: {
+ enqueued: true,
+ logicFunctionUniversalIdentifier: TARGET_UNIVERSAL_IDENTIFIER,
+ },
+ });
+
+describe('enqueueJob', () => {
+ let fetchSpy: MockInstance;
+
+ beforeEach(() => {
+ vi.resetModules();
+ process.env.TWENTY_API_URL = 'https://api.test';
+ process.env.TWENTY_APP_ACCESS_TOKEN = 'app-token';
+ fetchSpy = vi.spyOn(globalThis, 'fetch');
+ });
+
+ afterEach(() => {
+ delete process.env.TWENTY_API_URL;
+ delete process.env.TWENTY_APP_ACCESS_TOKEN;
+ fetchSpy.mockRestore();
+ });
+
+ it('calls the enqueueJob mutation on the metadata API and returns its result', async () => {
+ fetchSpy.mockResolvedValue(successResponse());
+
+ const enqueueJob = await importEnqueueJob();
+
+ const result = await enqueueJob({
+ logicFunctionUniversalIdentifier: TARGET_UNIVERSAL_IDENTIFIER,
+ payload: { batchIndex: 2 },
+ retryLimit: 3,
+ delayMs: 1000,
+ });
+
+ expect(result).toEqual({
+ enqueued: true,
+ logicFunctionUniversalIdentifier: TARGET_UNIVERSAL_IDENTIFIER,
+ });
+
+ const [url, requestInit] = fetchSpy.mock.calls[0];
+
+ expect(url).toBe('https://api.test/metadata');
+
+ const sentBody = JSON.parse(requestInit?.body as string);
+
+ expect(sentBody.query).toContain(
+ 'enqueueJob(input:$v1){enqueued,logicFunctionUniversalIdentifier}',
+ );
+ expect(Object.values(sentBody.variables)).toEqual([
+ {
+ logicFunctionUniversalIdentifier: TARGET_UNIVERSAL_IDENTIFIER,
+ payload: { batchIndex: 2 },
+ retryLimit: 3,
+ delayMs: 1000,
+ },
+ ]);
+ });
+
+ it('sends the app access token as a bearer credential', async () => {
+ fetchSpy.mockResolvedValue(successResponse());
+
+ const enqueueJob = await importEnqueueJob();
+
+ await enqueueJob({
+ logicFunctionUniversalIdentifier: TARGET_UNIVERSAL_IDENTIFIER,
+ });
+
+ const [, requestInit] = fetchSpy.mock.calls[0];
+ const headers = new Headers(requestInit?.headers);
+
+ expect(headers.get('authorization')).toBe('Bearer app-token');
+ });
+
+ it('surfaces GraphQL errors as a rejection', async () => {
+ fetchSpy.mockResolvedValue(
+ new Response(
+ JSON.stringify({ errors: [{ message: 'Logic function not found' }] }),
+ { status: 200, headers: { 'Content-Type': 'application/json' } },
+ ),
+ );
+
+ const enqueueJob = await importEnqueueJob();
+
+ await expect(
+ enqueueJob({
+ logicFunctionUniversalIdentifier: TARGET_UNIVERSAL_IDENTIFIER,
+ }),
+ ).rejects.toThrow(/Logic function not found/);
+ });
+});
diff --git a/packages/twenty-sdk/src/sdk/logic-function/jobs/enqueue-job.ts b/packages/twenty-sdk/src/sdk/logic-function/jobs/enqueue-job.ts
new file mode 100644
index 0000000000..e33d8f0493
--- /dev/null
+++ b/packages/twenty-sdk/src/sdk/logic-function/jobs/enqueue-job.ts
@@ -0,0 +1,21 @@
+import { MetadataApiClient } from 'twenty-client-sdk/metadata';
+import {
+ type EnqueueJobInput,
+ type EnqueueJobResult,
+} from 'twenty-shared/application';
+
+export const enqueueJob = async (
+ input: EnqueueJobInput,
+): Promise => {
+ const client = new MetadataApiClient();
+
+ const { enqueueJob: result } = await client.mutation({
+ enqueueJob: {
+ __args: { input },
+ enqueued: true,
+ logicFunctionUniversalIdentifier: true,
+ },
+ });
+
+ return result;
+};
diff --git a/packages/twenty-server/src/engine/core-modules/application/application-job/application-job.module.ts b/packages/twenty-server/src/engine/core-modules/application/application-job/application-job.module.ts
new file mode 100644
index 0000000000..fc5a4de829
--- /dev/null
+++ b/packages/twenty-server/src/engine/core-modules/application/application-job/application-job.module.ts
@@ -0,0 +1,12 @@
+import { Module } from '@nestjs/common';
+
+import { ApplicationJobResolver } from 'src/engine/core-modules/application/application-job/application-job.resolver';
+import { ApplicationJobService } from 'src/engine/core-modules/application/application-job/services/application-job.service';
+import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
+
+@Module({
+ imports: [WorkspaceCacheModule],
+ providers: [ApplicationJobService, ApplicationJobResolver],
+ exports: [ApplicationJobService],
+})
+export class ApplicationJobModule {}
diff --git a/packages/twenty-server/src/engine/core-modules/application/application-job/application-job.resolver.ts b/packages/twenty-server/src/engine/core-modules/application/application-job/application-job.resolver.ts
new file mode 100644
index 0000000000..8dd488fea6
--- /dev/null
+++ b/packages/twenty-server/src/engine/core-modules/application/application-job/application-job.resolver.ts
@@ -0,0 +1,46 @@
+import { UseFilters, UseGuards, UsePipes } from '@nestjs/common';
+import { Args, Mutation } from '@nestjs/graphql';
+
+import { type EnqueueJobResult } from 'twenty-shared/application';
+
+import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
+import { ApplicationExceptionFilter } from 'src/engine/core-modules/application/application-exception-filter';
+import { EnqueueJobResultDTO } from 'src/engine/core-modules/application/application-job/dtos/enqueue-job-result.dto';
+import { EnqueueJobInputDTO } from 'src/engine/core-modules/application/application-job/dtos/enqueue-job.input';
+import { ApplicationJobService } from 'src/engine/core-modules/application/application-job/services/application-job.service';
+import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
+import { type AuthContextUser } from 'src/engine/core-modules/auth/types/auth-context.type';
+import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
+import { type FlatWorkspace } from 'src/engine/core-modules/workspace/types/flat-workspace.type';
+import { AuthApplication } from 'src/engine/decorators/auth/auth-application.decorator';
+import { AuthUserWorkspaceId } from 'src/engine/decorators/auth/auth-user-workspace-id.decorator';
+import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
+import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
+import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
+import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
+
+@UseGuards(WorkspaceAuthGuard, NoPermissionGuard)
+@UseFilters(ApplicationExceptionFilter)
+@UsePipes(ResolverValidationPipe)
+@MetadataResolver()
+export class ApplicationJobResolver {
+ constructor(private readonly applicationJobService: ApplicationJobService) {}
+
+ @Mutation(() => EnqueueJobResultDTO)
+ async enqueueJob(
+ @AuthApplication() application: FlatApplication,
+ @AuthWorkspace() workspace: FlatWorkspace,
+ @AuthUser({ allowUndefined: true }) user: AuthContextUser | undefined,
+ @AuthUserWorkspaceId({ allowUndefined: true })
+ userWorkspaceId: string | undefined,
+ @Args('input') input: EnqueueJobInputDTO,
+ ): Promise {
+ return this.applicationJobService.enqueueJob({
+ applicationId: application.id,
+ workspaceId: workspace.id,
+ userId: user?.id ?? null,
+ userWorkspaceId: userWorkspaceId ?? null,
+ input,
+ });
+ }
+}
diff --git a/packages/twenty-server/src/engine/core-modules/application/application-job/constants/enqueue-job.constant.ts b/packages/twenty-server/src/engine/core-modules/application/application-job/constants/enqueue-job.constant.ts
new file mode 100644
index 0000000000..547ba74100
--- /dev/null
+++ b/packages/twenty-server/src/engine/core-modules/application/application-job/constants/enqueue-job.constant.ts
@@ -0,0 +1,8 @@
+export const ENQUEUE_JOB_MIN_RETRY_LIMIT = 0;
+export const ENQUEUE_JOB_MAX_RETRY_LIMIT = 10;
+export const ENQUEUE_JOB_DEFAULT_RETRY_LIMIT = 0;
+
+export const ENQUEUE_JOB_PRIORITY = 10;
+
+export const ENQUEUE_JOB_MIN_DELAY_MS = 0;
+export const ENQUEUE_JOB_MAX_DELAY_MS = 7 * 24 * 60 * 60 * 1000;
diff --git a/packages/twenty-server/src/engine/core-modules/application/application-job/dtos/enqueue-job-result.dto.ts b/packages/twenty-server/src/engine/core-modules/application/application-job/dtos/enqueue-job-result.dto.ts
new file mode 100644
index 0000000000..541f70a436
--- /dev/null
+++ b/packages/twenty-server/src/engine/core-modules/application/application-job/dtos/enqueue-job-result.dto.ts
@@ -0,0 +1,12 @@
+import { Field, ObjectType } from '@nestjs/graphql';
+
+import { type EnqueueJobResult } from 'twenty-shared/application';
+
+@ObjectType('EnqueueJobResult')
+export class EnqueueJobResultDTO implements EnqueueJobResult {
+ @Field()
+ enqueued: boolean;
+
+ @Field()
+ logicFunctionUniversalIdentifier: string;
+}
diff --git a/packages/twenty-server/src/engine/core-modules/application/application-job/dtos/enqueue-job.input.ts b/packages/twenty-server/src/engine/core-modules/application/application-job/dtos/enqueue-job.input.ts
new file mode 100644
index 0000000000..0cdccd41ba
--- /dev/null
+++ b/packages/twenty-server/src/engine/core-modules/application/application-job/dtos/enqueue-job.input.ts
@@ -0,0 +1,47 @@
+import { Field, InputType, Int } from '@nestjs/graphql';
+
+import {
+ IsInt,
+ IsNotEmpty,
+ IsObject,
+ IsOptional,
+ IsUUID,
+ Max,
+ Min,
+} from 'class-validator';
+import GraphQLJSON from 'graphql-type-json';
+import { type EnqueueJobInput } from 'twenty-shared/application';
+
+import {
+ ENQUEUE_JOB_MAX_DELAY_MS,
+ ENQUEUE_JOB_MAX_RETRY_LIMIT,
+ ENQUEUE_JOB_MIN_DELAY_MS,
+ ENQUEUE_JOB_MIN_RETRY_LIMIT,
+} from 'src/engine/core-modules/application/application-job/constants/enqueue-job.constant';
+
+@InputType('EnqueueJobInput')
+export class EnqueueJobInputDTO implements EnqueueJobInput {
+ @IsUUID()
+ @IsNotEmpty()
+ @Field()
+ logicFunctionUniversalIdentifier: string;
+
+ @IsObject()
+ @IsOptional()
+ @Field(() => GraphQLJSON, { nullable: true })
+ payload?: Record;
+
+ @IsInt()
+ @Min(ENQUEUE_JOB_MIN_RETRY_LIMIT)
+ @Max(ENQUEUE_JOB_MAX_RETRY_LIMIT)
+ @IsOptional()
+ @Field(() => Int, { nullable: true })
+ retryLimit?: number;
+
+ @IsInt()
+ @Min(ENQUEUE_JOB_MIN_DELAY_MS)
+ @Max(ENQUEUE_JOB_MAX_DELAY_MS)
+ @IsOptional()
+ @Field(() => Int, { nullable: true })
+ delayMs?: number;
+}
diff --git a/packages/twenty-server/src/engine/core-modules/application/application-job/services/__tests__/application-job.service.spec.ts b/packages/twenty-server/src/engine/core-modules/application/application-job/services/__tests__/application-job.service.spec.ts
new file mode 100644
index 0000000000..ab28084a80
--- /dev/null
+++ b/packages/twenty-server/src/engine/core-modules/application/application-job/services/__tests__/application-job.service.spec.ts
@@ -0,0 +1,200 @@
+import { ENQUEUE_JOB_PRIORITY } from 'src/engine/core-modules/application/application-job/constants/enqueue-job.constant';
+import { type EnqueueJobInputDTO } from 'src/engine/core-modules/application/application-job/dtos/enqueue-job.input';
+import { ApplicationJobService } from 'src/engine/core-modules/application/application-job/services/application-job.service';
+import {
+ ApplicationException,
+ ApplicationExceptionCode,
+} from 'src/engine/core-modules/application/application.exception';
+import { LogicFunctionTriggerJob } from 'src/engine/core-modules/logic-function/logic-function-trigger/jobs/logic-function-trigger.job';
+import { type MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
+import { type WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
+
+const APPLICATION_ID = 'c2a9e9d0-1f42-4f0e-9a0e-6d2e4b2a1f01';
+const WORKSPACE_ID = '20202020-1c25-4d02-bf25-6aeccf7ea419';
+const TARGET_UNIVERSAL_IDENTIFIER = '5a2f4d2a-1a1e-4c66-8a54-1f0a2b3c4d5e';
+const TARGET_LOGIC_FUNCTION_ID = 'ab6a2e5c-8c1f-4d0a-9bd1-52c1f5a9e100';
+
+const buildFlatLogicFunction = (
+ overrides: Partial<{
+ applicationId: string;
+ deletedAt: Date | null;
+ }> = {},
+) => ({
+ id: TARGET_LOGIC_FUNCTION_ID,
+ universalIdentifier: TARGET_UNIVERSAL_IDENTIFIER,
+ applicationId: APPLICATION_ID,
+ deletedAt: null,
+ ...overrides,
+});
+
+describe('ApplicationJobService', () => {
+ let service: ApplicationJobService;
+ let workspaceCacheService: jest.Mocked<
+ Pick
+ >;
+ let messageQueueService: jest.Mocked>;
+
+ const setCachedLogicFunctions = (
+ flatLogicFunctions: ReturnType[],
+ ) => {
+ workspaceCacheService.getOrRecompute.mockResolvedValue({
+ flatLogicFunctionMaps: {
+ byUniversalIdentifier: Object.fromEntries(
+ flatLogicFunctions.map((flatLogicFunction) => [
+ flatLogicFunction.universalIdentifier,
+ flatLogicFunction,
+ ]),
+ ),
+ },
+ // oxlint-disable-next-line typescript/no-explicit-any
+ } as any);
+ };
+
+ const enqueueJob = (
+ input: EnqueueJobInputDTO,
+ overrides: { userId?: string | null; userWorkspaceId?: string | null } = {},
+ ) =>
+ service.enqueueJob({
+ applicationId: APPLICATION_ID,
+ workspaceId: WORKSPACE_ID,
+ userId: overrides.userId ?? null,
+ userWorkspaceId: overrides.userWorkspaceId ?? null,
+ input,
+ });
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+
+ workspaceCacheService = { getOrRecompute: jest.fn() };
+ setCachedLogicFunctions([buildFlatLogicFunction()]);
+ messageQueueService = { add: jest.fn().mockResolvedValue(undefined) };
+
+ service = new ApplicationJobService(
+ workspaceCacheService as unknown as WorkspaceCacheService,
+ messageQueueService as unknown as MessageQueueService,
+ );
+ });
+
+ it('should enqueue a logic function trigger job with the provided job options', async () => {
+ const result = await enqueueJob({
+ logicFunctionUniversalIdentifier: TARGET_UNIVERSAL_IDENTIFIER,
+ payload: { batchIndex: 2 },
+ retryLimit: 3,
+ delayMs: 1000,
+ });
+
+ expect(result).toEqual({
+ enqueued: true,
+ logicFunctionUniversalIdentifier: TARGET_UNIVERSAL_IDENTIFIER,
+ });
+
+ expect(messageQueueService.add).toHaveBeenCalledWith(
+ LogicFunctionTriggerJob.name,
+ {
+ logicFunctionId: TARGET_LOGIC_FUNCTION_ID,
+ workspaceId: WORKSPACE_ID,
+ payload: { batchIndex: 2 },
+ },
+ { retryLimit: 3, priority: ENQUEUE_JOB_PRIORITY, delay: 1000 },
+ );
+ });
+
+ it('should always enqueue at the lowest priority so platform jobs go first', async () => {
+ await enqueueJob({
+ logicFunctionUniversalIdentifier: TARGET_UNIVERSAL_IDENTIFIER,
+ });
+
+ expect(messageQueueService.add).toHaveBeenCalledWith(
+ LogicFunctionTriggerJob.name,
+ expect.anything(),
+ expect.objectContaining({ priority: ENQUEUE_JOB_PRIORITY }),
+ );
+ });
+
+ it('should default to no retry and omit unset queue options', async () => {
+ await enqueueJob({
+ logicFunctionUniversalIdentifier: TARGET_UNIVERSAL_IDENTIFIER,
+ });
+
+ expect(messageQueueService.add).toHaveBeenCalledWith(
+ LogicFunctionTriggerJob.name,
+ {
+ logicFunctionId: TARGET_LOGIC_FUNCTION_ID,
+ workspaceId: WORKSPACE_ID,
+ payload: {},
+ },
+ { retryLimit: 0, priority: ENQUEUE_JOB_PRIORITY },
+ );
+ });
+
+ it('should forward the acting user so the queued run keeps the caller permissions', async () => {
+ await enqueueJob(
+ { logicFunctionUniversalIdentifier: TARGET_UNIVERSAL_IDENTIFIER },
+ { userId: 'user-1', userWorkspaceId: 'user-workspace-1' },
+ );
+
+ expect(messageQueueService.add).toHaveBeenCalledWith(
+ LogicFunctionTriggerJob.name,
+ expect.objectContaining({
+ userId: 'user-1',
+ userWorkspaceId: 'user-workspace-1',
+ }),
+ expect.anything(),
+ );
+ });
+
+ it('should resolve the target from the workspace cache instead of the database', async () => {
+ await enqueueJob({
+ logicFunctionUniversalIdentifier: TARGET_UNIVERSAL_IDENTIFIER,
+ });
+
+ expect(workspaceCacheService.getOrRecompute).toHaveBeenCalledWith(
+ WORKSPACE_ID,
+ ['flatLogicFunctionMaps'],
+ );
+ });
+
+ it('should throw LOGIC_FUNCTION_NOT_FOUND when the function belongs to another application', async () => {
+ setCachedLogicFunctions([
+ buildFlatLogicFunction({ applicationId: 'another-application-id' }),
+ ]);
+
+ await expect(
+ enqueueJob({
+ logicFunctionUniversalIdentifier: TARGET_UNIVERSAL_IDENTIFIER,
+ }),
+ ).rejects.toMatchObject({
+ code: ApplicationExceptionCode.LOGIC_FUNCTION_NOT_FOUND,
+ });
+
+ expect(messageQueueService.add).not.toHaveBeenCalled();
+ });
+
+ it('should throw LOGIC_FUNCTION_NOT_FOUND when the function is soft deleted', async () => {
+ setCachedLogicFunctions([
+ buildFlatLogicFunction({ deletedAt: new Date('2026-01-01') }),
+ ]);
+
+ await expect(
+ enqueueJob({
+ logicFunctionUniversalIdentifier: TARGET_UNIVERSAL_IDENTIFIER,
+ }),
+ ).rejects.toMatchObject({
+ code: ApplicationExceptionCode.LOGIC_FUNCTION_NOT_FOUND,
+ });
+
+ expect(messageQueueService.add).not.toHaveBeenCalled();
+ });
+
+ it('should throw LOGIC_FUNCTION_NOT_FOUND when the function is unknown', async () => {
+ setCachedLogicFunctions([]);
+
+ await expect(
+ enqueueJob({
+ logicFunctionUniversalIdentifier: TARGET_UNIVERSAL_IDENTIFIER,
+ }),
+ ).rejects.toThrow(ApplicationException);
+
+ expect(messageQueueService.add).not.toHaveBeenCalled();
+ });
+});
diff --git a/packages/twenty-server/src/engine/core-modules/application/application-job/services/application-job.service.ts b/packages/twenty-server/src/engine/core-modules/application/application-job/services/application-job.service.ts
new file mode 100644
index 0000000000..39767c7e0f
--- /dev/null
+++ b/packages/twenty-server/src/engine/core-modules/application/application-job/services/application-job.service.ts
@@ -0,0 +1,87 @@
+import { Injectable } from '@nestjs/common';
+
+import { type EnqueueJobResult } from 'twenty-shared/application';
+import { isDefined } from 'twenty-shared/utils';
+
+import {
+ ENQUEUE_JOB_DEFAULT_RETRY_LIMIT,
+ ENQUEUE_JOB_PRIORITY,
+} from 'src/engine/core-modules/application/application-job/constants/enqueue-job.constant';
+import { type EnqueueJobInputDTO } from 'src/engine/core-modules/application/application-job/dtos/enqueue-job.input';
+import {
+ ApplicationException,
+ ApplicationExceptionCode,
+} from 'src/engine/core-modules/application/application.exception';
+import {
+ LogicFunctionTriggerJob,
+ type LogicFunctionTriggerJobData,
+} from 'src/engine/core-modules/logic-function/logic-function-trigger/jobs/logic-function-trigger.job';
+import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
+import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
+import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
+import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util';
+import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
+
+@Injectable()
+export class ApplicationJobService {
+ constructor(
+ private readonly workspaceCacheService: WorkspaceCacheService,
+ @InjectMessageQueue(MessageQueue.logicFunctionQueue)
+ private readonly messageQueueService: MessageQueueService,
+ ) {}
+
+ async enqueueJob({
+ applicationId,
+ workspaceId,
+ userId,
+ userWorkspaceId,
+ input,
+ }: {
+ applicationId: string;
+ workspaceId: string;
+ userId: string | null;
+ userWorkspaceId: string | null;
+ input: EnqueueJobInputDTO;
+ }): Promise {
+ const { logicFunctionUniversalIdentifier } = input;
+
+ const { flatLogicFunctionMaps } =
+ await this.workspaceCacheService.getOrRecompute(workspaceId, [
+ 'flatLogicFunctionMaps',
+ ]);
+
+ const flatLogicFunction = findFlatEntityByUniversalIdentifier({
+ flatEntityMaps: flatLogicFunctionMaps,
+ universalIdentifier: logicFunctionUniversalIdentifier,
+ });
+
+ if (
+ !isDefined(flatLogicFunction) ||
+ isDefined(flatLogicFunction.deletedAt) ||
+ flatLogicFunction.applicationId !== applicationId
+ ) {
+ throw new ApplicationException(
+ `Logic function ${logicFunctionUniversalIdentifier} not found in this application`,
+ ApplicationExceptionCode.LOGIC_FUNCTION_NOT_FOUND,
+ );
+ }
+
+ await this.messageQueueService.add(
+ LogicFunctionTriggerJob.name,
+ {
+ logicFunctionId: flatLogicFunction.id,
+ workspaceId,
+ payload: input.payload ?? {},
+ ...(isDefined(userId) ? { userId } : {}),
+ ...(isDefined(userWorkspaceId) ? { userWorkspaceId } : {}),
+ },
+ {
+ retryLimit: input.retryLimit ?? ENQUEUE_JOB_DEFAULT_RETRY_LIMIT,
+ priority: ENQUEUE_JOB_PRIORITY,
+ ...(isDefined(input.delayMs) ? { delay: input.delayMs } : {}),
+ },
+ );
+
+ return { enqueued: true, logicFunctionUniversalIdentifier };
+ }
+}
diff --git a/packages/twenty-server/src/engine/metadata-modules/metadata-engine.module.ts b/packages/twenty-server/src/engine/metadata-modules/metadata-engine.module.ts
index a80a88fa44..969dc6a6d2 100644
--- a/packages/twenty-server/src/engine/metadata-modules/metadata-engine.module.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/metadata-engine.module.ts
@@ -7,6 +7,7 @@ import { AiChatModule } from 'src/engine/metadata-modules/ai/ai-chat/ai-chat.mod
import { AiGenerateTextModule } from 'src/engine/metadata-modules/ai/ai-generate-text/ai-generate-text.module';
import { AiWorkspaceStatsModule } from 'src/engine/metadata-modules/ai/ai-workspace-stats/ai-workspace-stats.module';
import { ApplicationConnectionsModule } from 'src/engine/core-modules/application/connection-provider/connections/application-connections.module';
+import { ApplicationJobModule } from 'src/engine/core-modules/application/application-job/application-job.module';
import { ApplicationKeyValueModule } from 'src/engine/core-modules/application/application-key-value/application-key-value.module';
import { CalendarChannelMetadataModule } from 'src/engine/metadata-modules/calendar-channel/calendar-channel-metadata.module';
import { ConnectedAccountMetadataModule } from 'src/engine/metadata-modules/connected-account/connected-account-metadata.module';
@@ -50,6 +51,7 @@ import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/work
AiGenerateTextModule,
AiWorkspaceStatsModule,
ApplicationConnectionsModule,
+ ApplicationJobModule,
ApplicationKeyValueModule,
MinimalMetadataModule,
ViewModule,
diff --git a/packages/twenty-server/test/integration/metadata/suites/application/enqueue-job.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/application/enqueue-job.integration-spec.ts
new file mode 100644
index 0000000000..db153b2766
--- /dev/null
+++ b/packages/twenty-server/test/integration/metadata/suites/application/enqueue-job.integration-spec.ts
@@ -0,0 +1,199 @@
+import { existsSync, mkdirSync, rmSync } from 'node:fs';
+import { tmpdir } from 'node:os';
+import { join } from 'node:path';
+
+import gql from 'graphql-tag';
+import { findManyApplications } from 'test/integration/graphql/utils/find-many-applications.util';
+import { generateApplicationToken } from 'test/integration/metadata/suites/application/utils/generate-application-token.util';
+import { createOneLogicFunction } from 'test/integration/metadata/suites/logic-function/utils/create-logic-function.util';
+import { deleteLogicFunction } from 'test/integration/metadata/suites/logic-function/utils/delete-logic-function.util';
+import { executeLogicFunction } from 'test/integration/metadata/suites/logic-function/utils/execute-logic-function.util';
+import { updateLogicFunctionSource } from 'test/integration/metadata/suites/logic-function/utils/update-logic-function-source.util';
+import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
+import { expectEventually } from 'test/integration/utils/expect-eventually.util';
+import { waitForAllJobsToFinish } from 'test/integration/utils/wait-for-all-jobs-to-finish.util';
+import { v4 as uuidv4 } from 'uuid';
+
+import { WORKSPACE_CUSTOM_APPLICATION_NAME } from 'src/engine/core-modules/application/constants/workspace-custom-application.constant';
+import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/twenty-standard-application/constants/twenty-standard-applications';
+
+const MARKER_DIRECTORY = join(tmpdir(), `enqueue-job-${uuidv4()}`);
+
+const TARGET_SOURCE_CODE = `import { writeFileSync } from 'node:fs';
+
+export const main = async (params: { markerPath: string }): Promise