From 1fea3f8228f064d32aa5860f118388611e2e12cf Mon Sep 17 00:00:00 2001 From: Thomas Trompette Date: Fri, 30 Jan 2026 18:22:07 +0100 Subject: [PATCH] Fix SSE for workflow show page (#17582) - Add SSE to workflow show page. Listens at workflow versions - Add a tool for creating trigger - Ensure step id is not used in params - Make AI adding step linked to the last node by default https://github.com/user-attachments/assets/41ee1835-9716-450e-82e8-54e1b63bd1ef --- ...ggerOptimisticEffectFromSseUpdateEvents.ts | 11 +++- .../components/WorkflowCard.tsx | 9 ++++ .../components/WorkflowSSESubscribeEffect.tsx | 24 +++++++++ ...date-workflow-version-trigger-input.dto.ts | 21 ++++++++ .../workflow-version-step.module.ts | 1 + .../workflow-tool.workspace-service.ts | 9 ++++ .../tools/create-complete-workflow.tool.ts | 4 +- .../create-workflow-version-step.tool.ts | 48 +++++++++++++++-- .../update-workflow-version-step.tool.ts | 7 +-- .../update-workflow-version-trigger.tool.ts | 51 +++++++++++++++++++ .../types/workflow-tool-dependencies.type.ts | 2 + 11 files changed, 173 insertions(+), 14 deletions(-) create mode 100644 packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowSSESubscribeEffect.tsx create mode 100644 packages/twenty-server/src/engine/core-modules/workflow/dtos/update-workflow-version-trigger-input.dto.ts create mode 100644 packages/twenty-server/src/modules/workflow/workflow-tools/tools/update-workflow-version-trigger.tool.ts diff --git a/packages/twenty-front/src/modules/sse-db-event/hooks/useTriggerOptimisticEffectFromSseUpdateEvents.ts b/packages/twenty-front/src/modules/sse-db-event/hooks/useTriggerOptimisticEffectFromSseUpdateEvents.ts index 2f1c2d8dcd..df4ce3bddb 100644 --- a/packages/twenty-front/src/modules/sse-db-event/hooks/useTriggerOptimisticEffectFromSseUpdateEvents.ts +++ b/packages/twenty-front/src/modules/sse-db-event/hooks/useTriggerOptimisticEffectFromSseUpdateEvents.ts @@ -10,6 +10,7 @@ import { generateDepthRecordGqlFieldsFromObject } from '@/object-record/graphql/ import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions'; import { useRefetchAggregateQueriesForObjectMetadataItem } from '@/object-record/hooks/useRefetchAggregateQueriesForObjectMetadataItem'; import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore'; +import { computeOptimisticRecordFromInput } from '@/object-record/utils/computeOptimisticRecordFromInput'; import { useCallback } from 'react'; import { isDefined, isNonEmptyArray } from 'twenty-shared/utils'; import { @@ -77,8 +78,14 @@ export const useTriggerOptimisticEffectFromSseUpdateEvents = () => { } const computedOptimisticRecord = { - ...cachedRecord, - ...updatedRecord, + ...computeOptimisticRecordFromInput({ + cache: apolloCoreClient.cache, + objectMetadataItem, + objectMetadataItems, + recordInput: updatedRecord, + objectPermissionsByObjectMetadataId, + currentWorkspaceMember: null, + }), id: updatedRecord.id, __typename: getObjectTypename(objectMetadataItem.nameSingular), }; diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowCard.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowCard.tsx index d7c6646b52..32ff76b5d9 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowCard.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowCard.tsx @@ -2,11 +2,17 @@ import { useTargetRecord } from '@/ui/layout/contexts/useTargetRecord'; import { getWorkflowVisualizerComponentInstanceId } from '@/workflow/utils/getWorkflowVisualizerComponentInstanceId'; import { WorkflowDiagramCanvasEditable } from '@/workflow/workflow-diagram/components/WorkflowDiagramCanvasEditable'; import { WorkflowDiagramEffect } from '@/workflow/workflow-diagram/components/WorkflowDiagramEffect'; +import { WorkflowSSESubscribeEffect } from '@/workflow/workflow-diagram/components/WorkflowSSESubscribeEffect'; import { WorkflowVisualizerEffect } from '@/workflow/workflow-diagram/components/WorkflowVisualizerEffect'; import { WorkflowVisualizerComponentInstanceContext } from '@/workflow/workflow-diagram/states/contexts/WorkflowVisualizerComponentInstanceContext'; +import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; +import { FeatureFlagKey } from '~/generated/graphql'; export const WorkflowCard = () => { const targetRecord = useTargetRecord(); + const isSseDbEventsEnabled = useIsFeatureEnabled( + FeatureFlagKey.IS_SSE_DB_EVENTS_ENABLED, + ); return ( { }} > + {isSseDbEventsEnabled && ( + + )} diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowSSESubscribeEffect.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowSSESubscribeEffect.tsx new file mode 100644 index 0000000000..3b19953fda --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowSSESubscribeEffect.tsx @@ -0,0 +1,24 @@ +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { useListenToObjectRecordEventsForQuery } from '@/sse-db-event/hooks/useListenToObjectRecordEventsForQuery'; + +export const WorkflowSSESubscribeEffect = ({ + workflowId, +}: { + workflowId: string; +}) => { + const queryId = `workflow-versions-for-workflow-${workflowId}`; + + useListenToObjectRecordEventsForQuery({ + queryId, + operationSignature: { + objectNameSingular: CoreObjectNameSingular.WorkflowVersion, + variables: { + filter: { + workflowId: { eq: workflowId }, + }, + }, + }, + }); + + return null; +}; diff --git a/packages/twenty-server/src/engine/core-modules/workflow/dtos/update-workflow-version-trigger-input.dto.ts b/packages/twenty-server/src/engine/core-modules/workflow/dtos/update-workflow-version-trigger-input.dto.ts new file mode 100644 index 0000000000..f16acf62b4 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/workflow/dtos/update-workflow-version-trigger-input.dto.ts @@ -0,0 +1,21 @@ +import { Field, InputType } from '@nestjs/graphql'; + +import graphqlTypeJson from 'graphql-type-json'; + +import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; +import { WorkflowTrigger } from 'src/modules/workflow/workflow-trigger/types/workflow-trigger.type'; + +@InputType() +export class UpdateWorkflowVersionTriggerInput { + @Field(() => UUIDScalarType, { + description: 'Workflow version ID', + nullable: false, + }) + workflowVersionId: string; + + @Field(() => graphqlTypeJson, { + description: 'Trigger to update in JSON format', + nullable: false, + }) + trigger: WorkflowTrigger; +} diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step.module.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step.module.ts index 54fc35e8d6..e3fff9f610 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step.module.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step.module.ts @@ -45,6 +45,7 @@ import { WorkflowVersionStepWorkspaceService } from 'src/modules/workflow/workfl exports: [ WorkflowVersionStepWorkspaceService, WorkflowVersionStepOperationsWorkspaceService, + WorkflowVersionStepHelpersWorkspaceService, ], }) export class WorkflowVersionStepModule {} diff --git a/packages/twenty-server/src/modules/workflow/workflow-tools/services/workflow-tool.workspace-service.ts b/packages/twenty-server/src/modules/workflow/workflow-tools/services/workflow-tool.workspace-service.ts index 63937d7d86..168011ab26 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-tools/services/workflow-tool.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-tools/services/workflow-tool.workspace-service.ts @@ -8,6 +8,7 @@ import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspac import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config'; import { WorkflowSchemaWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.workspace-service'; import { WorkflowVersionEdgeWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-version-edge/workflow-version-edge.workspace-service'; +import { WorkflowVersionStepHelpersWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step-helpers.workspace-service'; import { WorkflowVersionStepWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step.workspace-service'; import { WorkflowVersionWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-version/workflow-version.workspace-service'; import { @@ -26,6 +27,7 @@ import { createDeleteWorkflowVersionStepTool } from 'src/modules/workflow/workfl import { createGetWorkflowCurrentVersionTool } from 'src/modules/workflow/workflow-tools/tools/get-workflow-current-version.tool'; import { createUpdateWorkflowVersionPositionsTool } from 'src/modules/workflow/workflow-tools/tools/update-workflow-version-positions.tool'; import { createUpdateWorkflowVersionStepTool } from 'src/modules/workflow/workflow-tools/tools/update-workflow-version-step.tool'; +import { createUpdateWorkflowVersionTriggerTool } from 'src/modules/workflow/workflow-tools/tools/update-workflow-version-trigger.tool'; import { type WorkflowToolDependencies } from 'src/modules/workflow/workflow-tools/types/workflow-tool-dependencies.type'; import { WorkflowTriggerWorkspaceService } from 'src/modules/workflow/workflow-trigger/workspace-services/workflow-trigger.workspace-service'; @@ -36,6 +38,7 @@ export class WorkflowToolWorkspaceService { constructor( workflowVersionStepService: WorkflowVersionStepWorkspaceService, + workflowVersionStepHelpersService: WorkflowVersionStepHelpersWorkspaceService, workflowVersionEdgeService: WorkflowVersionEdgeWorkspaceService, workflowVersionService: WorkflowVersionWorkspaceService, workflowTriggerService: WorkflowTriggerWorkspaceService, @@ -46,6 +49,7 @@ export class WorkflowToolWorkspaceService { ) { this.deps = { workflowVersionStepService, + workflowVersionStepHelpersService, workflowVersionEdgeService, workflowVersionService, workflowTriggerService, @@ -79,6 +83,10 @@ export class WorkflowToolWorkspaceService { this.deps, context, ); + const updateWorkflowVersionTrigger = createUpdateWorkflowVersionTriggerTool( + this.deps, + context, + ); const deleteWorkflowVersionStep = createDeleteWorkflowVersionStepTool( this.deps, context, @@ -116,6 +124,7 @@ export class WorkflowToolWorkspaceService { [createCompleteWorkflow.name]: createCompleteWorkflow, [createWorkflowVersionStep.name]: createWorkflowVersionStep, [updateWorkflowVersionStep.name]: updateWorkflowVersionStep, + [updateWorkflowVersionTrigger.name]: updateWorkflowVersionTrigger, [deleteWorkflowVersionStep.name]: deleteWorkflowVersionStep, [createWorkflowVersionEdge.name]: createWorkflowVersionEdge, [deleteWorkflowVersionEdge.name]: deleteWorkflowVersionEdge, diff --git a/packages/twenty-server/src/modules/workflow/workflow-tools/tools/create-complete-workflow.tool.ts b/packages/twenty-server/src/modules/workflow/workflow-tools/tools/create-complete-workflow.tool.ts index 2b882e8bcb..ea04b86533 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-tools/tools/create-complete-workflow.tool.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-tools/tools/create-complete-workflow.tool.ts @@ -82,7 +82,7 @@ export const createCreateCompleteWorkflowTool = ( CRITICAL SCHEMA REQUIREMENTS: - Trigger type MUST be one of: DATABASE_EVENT, MANUAL, CRON, WEBHOOK - NEVER use "RECORD_CREATED" - this is invalid. Use "DATABASE_EVENT" instead. -- Each step MUST include: id, name, type, valid, settings +- Each step MUST include: id (must be a valid UUID), name, type, valid, settings - CREATE_RECORD actions MUST have objectName and objectRecord in settings.input - objectRecord must contain actual field values, not just field names - Use "trigger" as stepId for trigger step in stepPositions and edges @@ -97,7 +97,7 @@ IMPORTANT: The tool schema provides comprehensive field descriptions, examples, - Field requirements and data types - Common object patterns and field structures - Proper relationship field formats -- Variable reference syntax (e.g., {{trigger.object.fieldName}}) +- Variable reference syntax: {{trigger.fieldName}} for trigger data, {{.result.fieldName}} for step outputs (step-id is the step's UUID, not its name) - Error handling options This is the most efficient way for AI to create workflows as it handles all the complexity in one call.`, diff --git a/packages/twenty-server/src/modules/workflow/workflow-tools/tools/create-workflow-version-step.tool.ts b/packages/twenty-server/src/modules/workflow/workflow-tools/tools/create-workflow-version-step.tool.ts index 4f71b028e2..8a338e93f0 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-tools/tools/create-workflow-version-step.tool.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-tools/tools/create-workflow-version-step.tool.ts @@ -1,3 +1,5 @@ +import { isDefined } from 'twenty-shared/utils'; +import { TRIGGER_STEP_ID } from 'twenty-shared/workflow'; import { z } from 'zod'; import type { CreateWorkflowVersionStepInput } from 'src/engine/core-modules/workflow/dtos/create-workflow-version-step-input.dto'; @@ -17,7 +19,9 @@ const createWorkflowVersionStepSchema = z.object({ parentStepId: z .string() .optional() - .describe('Optional ID of the parent step this step should come after'), + .describe( + 'Optional ID of the parent step this step should come after. If not provided, the step will be added at the end of the workflow.', + ), parentStepConnectionOptions: z .object({ type: z.string().optional(), @@ -36,22 +40,56 @@ const createWorkflowVersionStepSchema = z.object({ }) .optional() .describe('Optional position coordinates for the step'), - id: z.string().optional().describe('Optional step ID'), }); export const createCreateWorkflowVersionStepTool = ( - deps: Pick, + deps: Pick< + WorkflowToolDependencies, + 'workflowVersionStepService' | 'workflowVersionStepHelpersService' + >, context: WorkflowToolContext, ) => ({ name: 'create_workflow_version_step' as const, description: - 'Create a new step in a workflow version. This adds a step to the specified workflow version with the given configuration.', + 'Create a new step in a workflow version. This adds a step to the specified workflow version with the given configuration. If parentStepId is not provided, the step will be appended at the end of the workflow.', inputSchema: createWorkflowVersionStepSchema, execute: async (parameters: CreateWorkflowVersionStepInput) => { try { + let effectiveParentStepId = parameters.parentStepId; + + if (!isDefined(effectiveParentStepId)) { + const workflowVersion = + await deps.workflowVersionStepHelpersService.getValidatedDraftWorkflowVersion( + { + workflowVersionId: parameters.workflowVersionId, + workspaceId: context.workspaceId, + }, + ); + + const steps = workflowVersion.steps ?? []; + + if (steps.length === 0) { + effectiveParentStepId = TRIGGER_STEP_ID; + } else { + const leafStep = steps.filter( + (step) => + !isDefined(step.nextStepIds) || step.nextStepIds.length === 0, + ); + + if (leafStep.length > 1) { + effectiveParentStepId = undefined; + } else { + effectiveParentStepId = leafStep[0]?.id; + } + } + } + return await deps.workflowVersionStepService.createWorkflowVersionStep({ workspaceId: context.workspaceId, - input: parameters, + input: { + ...parameters, + parentStepId: effectiveParentStepId, + }, }); } catch (error) { return { diff --git a/packages/twenty-server/src/modules/workflow/workflow-tools/tools/update-workflow-version-step.tool.ts b/packages/twenty-server/src/modules/workflow/workflow-tools/tools/update-workflow-version-step.tool.ts index 3256e4fc52..962d313ab0 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-tools/tools/update-workflow-version-step.tool.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-tools/tools/update-workflow-version-step.tool.ts @@ -1,7 +1,4 @@ -import { - workflowActionSchema, - workflowTriggerSchema, -} from 'twenty-shared/workflow'; +import { workflowActionSchema } from 'twenty-shared/workflow'; import { z } from 'zod'; import type { UpdateWorkflowVersionStepInput } from 'src/engine/core-modules/workflow/dtos/update-workflow-version-step-input.dto'; @@ -15,7 +12,7 @@ const updateWorkflowVersionStepSchema = z.object({ .string() .describe('The ID of the workflow version containing the step'), step: z - .union([workflowTriggerSchema, workflowActionSchema]) + .union([workflowActionSchema]) .describe('The updated step configuration'), }); diff --git a/packages/twenty-server/src/modules/workflow/workflow-tools/tools/update-workflow-version-trigger.tool.ts b/packages/twenty-server/src/modules/workflow/workflow-tools/tools/update-workflow-version-trigger.tool.ts new file mode 100644 index 0000000000..d5d495eb51 --- /dev/null +++ b/packages/twenty-server/src/modules/workflow/workflow-tools/tools/update-workflow-version-trigger.tool.ts @@ -0,0 +1,51 @@ +import { workflowTriggerSchema } from 'twenty-shared/workflow'; +import { z } from 'zod'; + +import type { UpdateWorkflowVersionTriggerInput } from 'src/engine/core-modules/workflow/dtos/update-workflow-version-trigger-input.dto'; +import { + type WorkflowToolContext, + type WorkflowToolDependencies, +} from 'src/modules/workflow/workflow-tools/types/workflow-tool-dependencies.type'; + +const updateWorkflowVersionTriggerSchema = z.object({ + workflowVersionId: z + .string() + .describe('The ID of the workflow version containing the trigger'), + trigger: workflowTriggerSchema.describe('The updated trigger configuration'), +}); + +export const createUpdateWorkflowVersionTriggerTool = ( + deps: Pick, + context: WorkflowToolContext, +) => ({ + name: 'update_workflow_version_trigger' as const, + description: + 'Update the trigger of a workflow version. This modifies the trigger configuration (e.g., changing trigger type, settings, or conditions).', + inputSchema: updateWorkflowVersionTriggerSchema, + execute: async (parameters: UpdateWorkflowVersionTriggerInput) => { + try { + await deps.workflowVersionStepHelpersService.getValidatedDraftWorkflowVersion( + { + workflowVersionId: parameters.workflowVersionId, + workspaceId: context.workspaceId, + }, + ); + + await deps.workflowVersionStepHelpersService.updateWorkflowVersionStepsAndTrigger( + { + workspaceId: context.workspaceId, + workflowVersionId: parameters.workflowVersionId, + trigger: parameters.trigger, + }, + ); + + return parameters.trigger; + } catch (error) { + return { + success: false, + error: error.message, + message: `Failed to update workflow version trigger: ${error.message}`, + }; + } + }, +}); diff --git a/packages/twenty-server/src/modules/workflow/workflow-tools/types/workflow-tool-dependencies.type.ts b/packages/twenty-server/src/modules/workflow/workflow-tools/types/workflow-tool-dependencies.type.ts index 37e5ac2a8b..adb710c13f 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-tools/types/workflow-tool-dependencies.type.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-tools/types/workflow-tool-dependencies.type.ts @@ -2,12 +2,14 @@ import type { RecordPositionService } from 'src/engine/core-modules/record-posit import type { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import type { WorkflowSchemaWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.workspace-service'; import type { WorkflowVersionEdgeWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-version-edge/workflow-version-edge.workspace-service'; +import type { WorkflowVersionStepHelpersWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step-helpers.workspace-service'; import type { WorkflowVersionStepWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step.workspace-service'; import type { WorkflowVersionWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-version/workflow-version.workspace-service'; import type { WorkflowTriggerWorkspaceService } from 'src/modules/workflow/workflow-trigger/workspace-services/workflow-trigger.workspace-service'; export type WorkflowToolDependencies = { workflowVersionStepService: WorkflowVersionStepWorkspaceService; + workflowVersionStepHelpersService: WorkflowVersionStepHelpersWorkspaceService; workflowVersionEdgeService: WorkflowVersionEdgeWorkspaceService; workflowVersionService: WorkflowVersionWorkspaceService; workflowTriggerService: WorkflowTriggerWorkspaceService;