refactor(workflow-tools): reorganize to one file per tool with co-located schemas (#16313)

## Summary

Reorganizes workflow tools to improve maintainability and
discoverability by having one file per tool with co-located input
schemas.

## Changes

- Create individual tool files in `tools/` directory (11 files)
- Co-locate input schemas with their tool implementations
- Add shared types file for dependencies and context
- Simplify workspace service to aggregate tool factories
- Remove centralized `schemas/` directory

## New Structure

```
workflow-tools/
├── services/
│   └── workflow-tool.workspace-service.ts
├── tools/
│   ├── activate-workflow-version.tool.ts
│   ├── compute-step-output-schema.tool.ts
│   ├── create-complete-workflow.tool.ts
│   ├── create-draft-from-workflow-version.tool.ts
│   ├── create-workflow-version-edge.tool.ts
│   ├── create-workflow-version-step.tool.ts
│   ├── deactivate-workflow-version.tool.ts
│   ├── delete-workflow-version-edge.tool.ts
│   ├── delete-workflow-version-step.tool.ts
│   ├── update-workflow-version-positions.tool.ts
│   └── update-workflow-version-step.tool.ts
├── types/
│   └── workflow-tool-dependencies.type.ts
└── workflow-tools.module.ts
```

## Benefits

- **Co-location**: Schema and tool logic are in the same file
- **Single responsibility**: Each file handles one tool
- **Easier maintenance**: Changes to a tool only touch one file
- **Better discoverability**: File names match tool names
This commit is contained in:
Félix Malfait
2025-12-04 21:06:49 +01:00
committed by GitHub
parent 1991ee850e
commit 9cecbaebc3
46 changed files with 2323 additions and 1186 deletions
@@ -0,0 +1,94 @@
import { type ToolSet } from 'ai';
import { type RestrictedFieldsPermissions } from 'twenty-shared/types';
import { z } from 'zod';
import { type ObjectMetadataForToolSchema } from 'src/engine/core-modules/record-crud/types/object-metadata-for-tool-schema.type';
import { generateRecordPropertiesZodSchema } from 'src/engine/core-modules/record-crud/zod-schemas/record-properties.zod-schema';
import { type ToolGeneratorContext } from 'src/engine/core-modules/tool-generator/types/tool-generator.types';
import { WorkflowActionType } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action-type.enum';
import {
createAndConfigureStep,
type WorkflowStepToolsDeps,
} from './step-builder.utils';
export function buildCreateRecordStepTool(
deps: WorkflowStepToolsDeps,
objectMetadata: ObjectMetadataForToolSchema,
restrictedFields: RestrictedFieldsPermissions,
context: ToolGeneratorContext,
): ToolSet {
const recordPropertiesSchema = generateRecordPropertiesZodSchema(
objectMetadata,
false,
restrictedFields,
);
const inputSchema = z.object({
workflowVersionId: z
.string()
.describe('The ID of the workflow version to add the step to'),
parentStepId: z
.string()
.optional()
.describe('Optional ID of the parent step this step should come after'),
stepName: z
.string()
.optional()
.describe(
`Name for this step (default: "Create ${objectMetadata.labelSingular}")`,
),
input: recordPropertiesSchema.describe(
`The ${objectMetadata.labelSingular} record data. Use {{trigger.fieldName}} or {{stepId.fieldName}} syntax to reference dynamic values from previous steps.`,
),
});
return {
[`configure_create_${objectMetadata.nameSingular}_step`]: {
description:
`Add a workflow step that creates a ${objectMetadata.labelSingular} record. ` +
`Provide the record fields directly - use {{trigger.fieldName}} or {{stepId.fieldName}} to reference values from previous steps.`,
inputSchema,
execute: async (parameters: z.infer<typeof inputSchema>) => {
try {
const { stepId, result } = await createAndConfigureStep(
deps,
context.workspaceId,
parameters.workflowVersionId,
WorkflowActionType.CREATE_RECORD,
parameters.parentStepId,
{
name:
parameters.stepName || `Create ${objectMetadata.labelSingular}`,
type: WorkflowActionType.CREATE_RECORD,
valid: true,
settings: {
input: {
objectName: objectMetadata.nameSingular,
objectRecord: parameters.input,
},
outputSchema: {},
errorHandlingOptions: {
retryOnFailure: { value: false },
continueOnFailure: { value: false },
},
},
},
);
return {
success: true,
message: `Created workflow step to create ${objectMetadata.labelSingular}`,
result: { stepId, step: result },
};
} catch (error) {
return {
success: false,
error: error.message,
message: `Failed to create ${objectMetadata.labelSingular} workflow step: ${error.message}`,
};
}
},
},
};
}
@@ -0,0 +1,87 @@
import { type ToolSet } from 'ai';
import { z } from 'zod';
import { type ObjectMetadataForToolSchema } from 'src/engine/core-modules/record-crud/types/object-metadata-for-tool-schema.type';
import { type ToolGeneratorContext } from 'src/engine/core-modules/tool-generator/types/tool-generator.types';
import { WorkflowActionType } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action-type.enum';
import {
createAndConfigureStep,
type WorkflowStepToolsDeps,
} from './step-builder.utils';
export function buildDeleteRecordStepTool(
deps: WorkflowStepToolsDeps,
objectMetadata: ObjectMetadataForToolSchema,
context: ToolGeneratorContext,
): ToolSet {
const inputSchema = z.object({
workflowVersionId: z
.string()
.describe('The ID of the workflow version to add the step to'),
parentStepId: z
.string()
.optional()
.describe('Optional ID of the parent step this step should come after'),
stepName: z
.string()
.optional()
.describe(
`Name for this step (default: "Delete ${objectMetadata.labelSingular}")`,
),
objectRecordId: z
.string()
.describe(
`The ID of the ${objectMetadata.labelSingular} record to delete. Use {{trigger.id}} or {{stepId.result.id}} to reference a dynamic ID.`,
),
});
return {
[`configure_delete_${objectMetadata.nameSingular}_step`]: {
description:
`Add a workflow step that deletes a ${objectMetadata.labelSingular} record. ` +
`This performs a soft delete (marks as deleted but preserves data).`,
inputSchema,
execute: async (parameters: z.infer<typeof inputSchema>) => {
try {
const { stepId, result } = await createAndConfigureStep(
deps,
context.workspaceId,
parameters.workflowVersionId,
WorkflowActionType.DELETE_RECORD,
parameters.parentStepId,
{
name:
parameters.stepName || `Delete ${objectMetadata.labelSingular}`,
type: WorkflowActionType.DELETE_RECORD,
valid: true,
settings: {
input: {
objectName: objectMetadata.nameSingular,
objectRecordId: parameters.objectRecordId,
},
outputSchema: {},
errorHandlingOptions: {
retryOnFailure: { value: false },
continueOnFailure: { value: false },
},
},
},
);
return {
success: true,
message: `Created workflow step to delete ${objectMetadata.labelSingular}`,
result: { stepId, step: result },
};
} catch (error) {
return {
success: false,
error: error.message,
message: `Failed to create ${objectMetadata.labelSingular} delete workflow step: ${error.message}`,
};
}
},
},
};
}
@@ -0,0 +1,149 @@
import { type ToolSet } from 'ai';
import {
FieldMetadataType,
RelationType,
type RestrictedFieldsPermissions,
} from 'twenty-shared/types';
import { z } from 'zod';
import { type ObjectMetadataForToolSchema } from 'src/engine/core-modules/record-crud/types/object-metadata-for-tool-schema.type';
import { generateFieldFilterZodSchema } from 'src/engine/core-modules/record-crud/zod-schemas/field-filters.zod-schema';
import { ObjectRecordOrderBySchema } from 'src/engine/core-modules/record-crud/zod-schemas/order-by.zod-schema';
import { type ToolGeneratorContext } from 'src/engine/core-modules/tool-generator/types/tool-generator.types';
import { shouldExcludeFieldFromAgentToolSchema } from 'src/engine/metadata-modules/field-metadata/utils/should-exclude-field-from-agent-tool-schema.util';
import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
import { WorkflowActionType } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action-type.enum';
import {
createAndConfigureStep,
type WorkflowStepToolsDeps,
} from './step-builder.utils';
export function buildFindRecordsStepTool(
deps: WorkflowStepToolsDeps,
objectMetadata: ObjectMetadataForToolSchema,
restrictedFields: RestrictedFieldsPermissions,
context: ToolGeneratorContext,
): ToolSet {
const filterShape: Record<string, z.ZodTypeAny> = {};
objectMetadata.fields.forEach((field) => {
if (shouldExcludeFieldFromAgentToolSchema(field)) {
return;
}
if (restrictedFields?.[field.id]?.canRead === false) {
return;
}
const filterSchema = generateFieldFilterZodSchema(field);
if (!filterSchema) {
return;
}
const isManyToOneRelationField =
isFieldMetadataEntityOfType(field, FieldMetadataType.RELATION) &&
field.settings?.relationType === RelationType.MANY_TO_ONE;
filterShape[isManyToOneRelationField ? `${field.name}Id` : field.name] =
filterSchema;
});
const inputSchema = z.object({
workflowVersionId: z
.string()
.describe('The ID of the workflow version to add the step to'),
parentStepId: z
.string()
.optional()
.describe('Optional ID of the parent step this step should come after'),
stepName: z
.string()
.optional()
.describe(
`Name for this step (default: "Find ${objectMetadata.labelPlural}")`,
),
limit: z
.number()
.int()
.positive()
.max(1000)
.optional()
.default(100)
.describe('Maximum number of records to return (default: 100)'),
orderBy: ObjectRecordOrderBySchema.optional().describe(
'Sort records by field(s). Each item is an object with field name as key, sort direction as value.',
),
filter: z
.object(filterShape)
.partial()
.optional()
.describe(
`Filter criteria for ${objectMetadata.labelPlural}. Use {{trigger.fieldName}} or {{stepId.fieldName}} to reference dynamic values.`,
),
});
return {
[`configure_find_${objectMetadata.namePlural}_step`]: {
description:
`Add a workflow step that searches for ${objectMetadata.labelPlural} records. ` +
`Results can be used in subsequent steps via {{stepId.result}}.`,
inputSchema,
execute: async (parameters: z.infer<typeof inputSchema>) => {
try {
const filterConfig = parameters.filter
? {
recordFilters: Object.entries(parameters.filter).map(
([fieldName, filterValue]) => ({
fieldName,
filter: filterValue,
}),
),
}
: undefined;
const { stepId, result } = await createAndConfigureStep(
deps,
context.workspaceId,
parameters.workflowVersionId,
WorkflowActionType.FIND_RECORDS,
parameters.parentStepId,
{
name: parameters.stepName || `Find ${objectMetadata.labelPlural}`,
type: WorkflowActionType.FIND_RECORDS,
valid: true,
settings: {
input: {
objectName: objectMetadata.nameSingular,
limit: parameters.limit,
filter: filterConfig,
orderBy: parameters.orderBy
? { gqlOperationOrderBy: parameters.orderBy }
: undefined,
},
outputSchema: {},
errorHandlingOptions: {
retryOnFailure: { value: false },
continueOnFailure: { value: false },
},
},
},
);
return {
success: true,
message: `Created workflow step to find ${objectMetadata.labelPlural}`,
result: { stepId, step: result },
};
} catch (error) {
return {
success: false,
error: error.message,
message: `Failed to create ${objectMetadata.labelPlural} find workflow step: ${error.message}`,
};
}
},
},
};
}
@@ -0,0 +1,52 @@
import { v4 as uuidv4 } from 'uuid';
import { type WorkflowActionType } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action-type.enum';
export type WorkflowStepToolsDeps = {
workflowVersionStepService: {
createWorkflowVersionStep: (args: {
workspaceId: string;
input: {
workflowVersionId: string;
stepType: WorkflowActionType;
parentStepId?: string;
id?: string;
};
}) => Promise<unknown>;
updateWorkflowVersionStep: (args: {
workspaceId: string;
workflowVersionId: string;
step: unknown;
}) => Promise<unknown>;
};
};
export async function createAndConfigureStep(
deps: WorkflowStepToolsDeps,
workspaceId: string,
workflowVersionId: string,
stepType: WorkflowActionType,
parentStepId: string | undefined,
stepConfig: object,
) {
const stepId = uuidv4();
await deps.workflowVersionStepService.createWorkflowVersionStep({
workspaceId,
input: {
workflowVersionId,
stepType,
parentStepId,
id: stepId,
},
});
const result =
await deps.workflowVersionStepService.updateWorkflowVersionStep({
workspaceId,
workflowVersionId,
step: { id: stepId, ...stepConfig },
});
return { stepId, result };
}
@@ -0,0 +1,101 @@
import { type ToolSet } from 'ai';
import { type RestrictedFieldsPermissions } from 'twenty-shared/types';
import { z } from 'zod';
import { type ObjectMetadataForToolSchema } from 'src/engine/core-modules/record-crud/types/object-metadata-for-tool-schema.type';
import { generateRecordPropertiesZodSchema } from 'src/engine/core-modules/record-crud/zod-schemas/record-properties.zod-schema';
import { type ToolGeneratorContext } from 'src/engine/core-modules/tool-generator/types/tool-generator.types';
import { WorkflowActionType } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action-type.enum';
import {
createAndConfigureStep,
type WorkflowStepToolsDeps,
} from './step-builder.utils';
export function buildUpdateRecordStepTool(
deps: WorkflowStepToolsDeps,
objectMetadata: ObjectMetadataForToolSchema,
restrictedFields: RestrictedFieldsPermissions,
context: ToolGeneratorContext,
): ToolSet {
const recordPropertiesSchema = generateRecordPropertiesZodSchema(
objectMetadata,
false,
restrictedFields,
);
const updateSchema = recordPropertiesSchema.partial();
const inputSchema = z.object({
workflowVersionId: z
.string()
.describe('The ID of the workflow version to add the step to'),
parentStepId: z
.string()
.optional()
.describe('Optional ID of the parent step this step should come after'),
stepName: z
.string()
.optional()
.describe(
`Name for this step (default: "Update ${objectMetadata.labelSingular}")`,
),
objectRecordId: z
.string()
.describe(
`The ID of the ${objectMetadata.labelSingular} record to update. Use {{trigger.id}} or {{stepId.result.id}} to reference a dynamic ID.`,
),
fieldsToUpdate: updateSchema.describe(
`The fields to update on the ${objectMetadata.labelSingular} record. Only include fields you want to change. Use {{trigger.fieldName}} or {{stepId.fieldName}} to reference dynamic values.`,
),
});
return {
[`configure_update_${objectMetadata.nameSingular}_step`]: {
description:
`Add a workflow step that updates an existing ${objectMetadata.labelSingular} record. ` +
`Specify the record ID and only the fields you want to update.`,
inputSchema,
execute: async (parameters: z.infer<typeof inputSchema>) => {
try {
const { stepId, result } = await createAndConfigureStep(
deps,
context.workspaceId,
parameters.workflowVersionId,
WorkflowActionType.UPDATE_RECORD,
parameters.parentStepId,
{
name:
parameters.stepName || `Update ${objectMetadata.labelSingular}`,
type: WorkflowActionType.UPDATE_RECORD,
valid: true,
settings: {
input: {
objectName: objectMetadata.nameSingular,
objectRecordId: parameters.objectRecordId,
objectRecord: parameters.fieldsToUpdate,
},
outputSchema: {},
errorHandlingOptions: {
retryOnFailure: { value: false },
continueOnFailure: { value: false },
},
},
},
);
return {
success: true,
message: `Created workflow step to update ${objectMetadata.labelSingular}`,
result: { stepId, step: result },
};
} catch (error) {
return {
success: false,
error: error.message,
message: `Failed to create ${objectMetadata.labelSingular} update workflow step: ${error.message}`,
};
}
},
},
};
}
@@ -0,0 +1,75 @@
import { type ToolSet } from 'ai';
import {
type ObjectWithPermission,
type ToolGeneratorContext,
} from 'src/engine/core-modules/tool-generator/types/tool-generator.types';
import { buildCreateRecordStepTool } from './builders/create-record-step.builder';
import { buildDeleteRecordStepTool } from './builders/delete-record-step.builder';
import { buildFindRecordsStepTool } from './builders/find-records-step.builder';
import { type WorkflowStepToolsDeps } from './builders/step-builder.utils';
import { buildUpdateRecordStepTool } from './builders/update-record-step.builder';
export { type WorkflowStepToolsDeps } from './builders/step-builder.utils';
export const createWorkflowStepToolsFactory = (deps: WorkflowStepToolsDeps) => {
return (
{
objectMetadata,
restrictedFields,
canCreate,
canRead,
canUpdate,
canDelete,
}: ObjectWithPermission,
context: ToolGeneratorContext,
): ToolSet => {
const tools: ToolSet = {};
if (canCreate) {
Object.assign(
tools,
buildCreateRecordStepTool(
deps,
objectMetadata,
restrictedFields,
context,
),
);
}
if (canUpdate) {
Object.assign(
tools,
buildUpdateRecordStepTool(
deps,
objectMetadata,
restrictedFields,
context,
),
);
}
if (canRead) {
Object.assign(
tools,
buildFindRecordsStepTool(
deps,
objectMetadata,
restrictedFields,
context,
),
);
}
if (canDelete) {
Object.assign(
tools,
buildDeleteRecordStepTool(deps, objectMetadata, context),
);
}
return tools;
};
};
@@ -1,143 +0,0 @@
import {
workflowActionSchema,
workflowTriggerSchema,
} from 'twenty-shared/workflow';
import { z } from 'zod';
import { WorkflowActionType } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
export const createWorkflowVersionStepSchema = z.object({
workflowVersionId: z
.string()
.describe('The ID of the workflow version to add the step to'),
stepType: z
.enum(Object.values(WorkflowActionType) as [string, ...string[]])
.describe('The type of step to create'),
parentStepId: z
.string()
.optional()
.describe('Optional ID of the parent step this step should come after'),
nextStepId: z
.string()
.optional()
.describe('Optional ID of the step this new step should connect to'),
position: z
.object({
x: z.number(),
y: z.number(),
})
.optional()
.describe('Optional position coordinates for the step'),
});
export const updateWorkflowVersionStepSchema = z.object({
workflowVersionId: z
.string()
.describe('The ID of the workflow version containing the step'),
step: z
.union([workflowTriggerSchema, workflowActionSchema])
.describe('The updated step configuration'),
});
export const deleteWorkflowVersionStepSchema = z.object({
workflowVersionId: z
.string()
.describe('The ID of the workflow version containing the step'),
stepId: z.string().describe('The ID of the step to delete'),
});
export const createWorkflowVersionEdgeSchema = z.object({
workflowVersionId: z.string().describe('The ID of the workflow version'),
source: z.string().describe('The ID of the source step'),
target: z.string().describe('The ID of the target step'),
});
export const deleteWorkflowVersionEdgeSchema = z.object({
workflowVersionId: z.string().describe('The ID of the workflow version'),
source: z.string().describe('The ID of the source step'),
target: z.string().describe('The ID of the target step'),
});
export const createDraftFromWorkflowVersionSchema = z.object({
workflowId: z.string().describe('The ID of the workflow'),
workflowVersionIdToCopy: z
.string()
.describe('The ID of the workflow version to create a draft from'),
});
export const updateWorkflowVersionPositionsSchema = z.object({
workflowVersionId: z.string().describe('The ID of the workflow version'),
positions: z
.array(
z.object({
stepId: z.string(),
position: z.object({
x: z.number(),
y: z.number(),
}),
}),
)
.describe('Array of step positions to update'),
});
export const activateWorkflowVersionSchema = z.object({
workflowVersionId: z
.string()
.describe('The ID of the workflow version to activate'),
});
export const deactivateWorkflowVersionSchema = z.object({
workflowVersionId: z
.string()
.describe('The ID of the workflow version to deactivate'),
});
export const computeStepOutputSchemaSchema = z.object({
step: z
.union([workflowTriggerSchema, workflowActionSchema])
.describe('The workflow step configuration'),
workflowVersionId: z.string().describe('The ID of the workflow version'),
});
export const createCompleteWorkflowSchema = z.object({
name: z.string().describe('The name of the workflow'),
description: z
.string()
.optional()
.describe('Optional description of the workflow'),
trigger: workflowTriggerSchema,
steps: z
.array(workflowActionSchema)
.describe('Array of workflow action steps'),
stepPositions: z
.array(
z.object({
stepId: z
.string()
.describe('The ID of the step (use "trigger" for trigger step)'),
position: z.object({
x: z.number().describe('X coordinate for the step position'),
y: z.number().describe('Y coordinate for the step position'),
}),
}),
)
.optional()
.describe('Optional array of step positions for layout'),
edges: z
.array(
z.object({
source: z
.string()
.describe(
'The ID of the source step (use "trigger" for trigger step)',
),
target: z.string().describe('The ID of the target step'),
}),
)
.optional()
.describe('Optional array of connections between steps'),
activate: z
.boolean()
.optional()
.describe('Whether to activate the workflow immediately (default: false)'),
});
@@ -1,510 +1,143 @@
import { Injectable } from '@nestjs/common';
import { type ToolSet } from 'ai';
import { v4 as uuidv4 } from 'uuid';
import { RecordPositionService } from 'src/engine/core-modules/record-position/services/record-position.service';
import type { CreateWorkflowVersionStepInput } from 'src/engine/core-modules/workflow/dtos/create-workflow-version-step-input.dto';
import type { UpdateWorkflowVersionPositionsInput } from 'src/engine/core-modules/workflow/dtos/update-workflow-version-positions-input.dto';
import type { UpdateWorkflowVersionStepInput } from 'src/engine/core-modules/workflow/dtos/update-workflow-version-step-input.dto';
import { PerObjectToolGeneratorService } from 'src/engine/core-modules/tool-generator/services/per-object-tool-generator.service';
import { type ToolHints } from 'src/engine/metadata-modules/ai/ai-chat-router/types/tool-hints.interface';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config';
import { WorkflowVersionStatus } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity';
import { WorkflowStatus } from 'src/modules/workflow/common/standard-objects/workflow.workspace-entity';
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 { 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 { type WorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
import {
activateWorkflowVersionSchema,
computeStepOutputSchemaSchema,
createCompleteWorkflowSchema,
createDraftFromWorkflowVersionSchema,
createWorkflowVersionEdgeSchema,
createWorkflowVersionStepSchema,
deactivateWorkflowVersionSchema,
deleteWorkflowVersionEdgeSchema,
deleteWorkflowVersionStepSchema,
updateWorkflowVersionPositionsSchema,
updateWorkflowVersionStepSchema,
} from 'src/modules/workflow/workflow-tools/schemas/workflow-tool-schemas';
import { type WorkflowTrigger } from 'src/modules/workflow/workflow-trigger/types/workflow-trigger.type';
createWorkflowStepToolsFactory,
type WorkflowStepToolsDeps,
} from 'src/modules/workflow/workflow-tools/factories/workflow-step-tools.factory';
import { createActivateWorkflowVersionTool } from 'src/modules/workflow/workflow-tools/tools/activate-workflow-version.tool';
import { createComputeStepOutputSchemaTool } from 'src/modules/workflow/workflow-tools/tools/compute-step-output-schema.tool';
import { createCreateCompleteWorkflowTool } from 'src/modules/workflow/workflow-tools/tools/create-complete-workflow.tool';
import { createCreateDraftFromWorkflowVersionTool } from 'src/modules/workflow/workflow-tools/tools/create-draft-from-workflow-version.tool';
import { createCreateWorkflowVersionEdgeTool } from 'src/modules/workflow/workflow-tools/tools/create-workflow-version-edge.tool';
import { createCreateWorkflowVersionStepTool } from 'src/modules/workflow/workflow-tools/tools/create-workflow-version-step.tool';
import { createDeactivateWorkflowVersionTool } from 'src/modules/workflow/workflow-tools/tools/deactivate-workflow-version.tool';
import { createDeleteWorkflowVersionEdgeTool } from 'src/modules/workflow/workflow-tools/tools/delete-workflow-version-edge.tool';
import { createDeleteWorkflowVersionStepTool } from 'src/modules/workflow/workflow-tools/tools/delete-workflow-version-step.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 { 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';
@Injectable()
export class WorkflowToolWorkspaceService {
constructor(
private readonly workflowVersionStepService: WorkflowVersionStepWorkspaceService,
private readonly workflowVersionEdgeService: WorkflowVersionEdgeWorkspaceService,
private readonly workflowVersionService: WorkflowVersionWorkspaceService,
private readonly workflowTriggerService: WorkflowTriggerWorkspaceService,
private readonly workflowSchemaService: WorkflowSchemaWorkspaceService,
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly recordPositionService: RecordPositionService,
) {}
private readonly deps: WorkflowToolDependencies;
private readonly workflowStepToolsDeps: WorkflowStepToolsDeps;
constructor(
workflowVersionStepService: WorkflowVersionStepWorkspaceService,
workflowVersionEdgeService: WorkflowVersionEdgeWorkspaceService,
workflowVersionService: WorkflowVersionWorkspaceService,
workflowTriggerService: WorkflowTriggerWorkspaceService,
workflowSchemaService: WorkflowSchemaWorkspaceService,
twentyORMGlobalManager: TwentyORMGlobalManager,
recordPositionService: RecordPositionService,
private readonly perObjectToolGenerator: PerObjectToolGeneratorService,
) {
this.deps = {
workflowVersionStepService,
workflowVersionEdgeService,
workflowVersionService,
workflowTriggerService,
workflowSchemaService,
twentyORMGlobalManager,
recordPositionService,
};
this.workflowStepToolsDeps = {
workflowVersionStepService,
};
}
// Generates static workflow tools that don't depend on workspace objects
generateWorkflowTools(
workspaceId: string,
rolePermissionConfig: RolePermissionConfig,
): ToolSet {
const tools: ToolSet = {};
const context = { workspaceId };
const contextWithPermissions = { workspaceId, rolePermissionConfig };
tools.create_complete_workflow = {
description: `Create a complete workflow with trigger, steps, and connections in a single operation.
const createCompleteWorkflow = createCreateCompleteWorkflowTool(
this.deps,
contextWithPermissions,
);
const createWorkflowVersionStep = createCreateWorkflowVersionStepTool(
this.deps,
context,
);
const updateWorkflowVersionStep = createUpdateWorkflowVersionStepTool(
this.deps,
context,
);
const deleteWorkflowVersionStep = createDeleteWorkflowVersionStepTool(
this.deps,
context,
);
const createWorkflowVersionEdge = createCreateWorkflowVersionEdgeTool(
this.deps,
context,
);
const deleteWorkflowVersionEdge = createDeleteWorkflowVersionEdgeTool(
this.deps,
context,
);
const createDraftFromWorkflowVersion =
createCreateDraftFromWorkflowVersionTool(this.deps, context);
const updateWorkflowVersionPositions =
createUpdateWorkflowVersionPositionsTool(this.deps, context);
const activateWorkflowVersion = createActivateWorkflowVersionTool(
this.deps,
);
const deactivateWorkflowVersion = createDeactivateWorkflowVersionTool(
this.deps,
);
const computeStepOutputSchema = createComputeStepOutputSchemaTool(
this.deps,
context,
);
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
- 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
Common mistakes to avoid:
- Using "RECORD_CREATED" instead of "DATABASE_EVENT"
- Missing the "name" and "valid" fields in steps
- Missing the "objectRecord" field in CREATE_RECORD actions
- Using "fieldsToUpdate" instead of "objectRecord" in CREATE_RECORD actions
IMPORTANT: The tool schema provides comprehensive field descriptions, examples, and validation rules. Always refer to the schema for:
- Field requirements and data types
- Common object patterns and field structures
- Proper relationship field formats
- Variable reference syntax (e.g., {{trigger.object.fieldName}})
- Error handling options
This is the most efficient way for AI to create workflows as it handles all the complexity in one call.`,
inputSchema: createCompleteWorkflowSchema,
execute: async (parameters: {
name: string;
description?: string;
trigger: WorkflowTrigger;
steps: WorkflowAction[];
stepPositions?: Array<{
stepId: string;
position: { x: number; y: number };
}>;
edges?: Array<{ source: string; target: string }>;
activate?: boolean;
}) => {
try {
const workflowId = await this.createWorkflow({
workspaceId,
name: parameters.name,
rolePermissionConfig,
});
const workflowVersionId = await this.createWorkflowVersion({
workspaceId,
workflowId,
trigger: parameters.trigger,
steps: parameters.steps,
rolePermissionConfig,
});
if (parameters.stepPositions && parameters.stepPositions.length > 0) {
const positions = parameters.stepPositions.map((pos) => ({
id: pos.stepId === 'trigger' ? 'trigger' : pos.stepId,
position: pos.position,
}));
await this.workflowVersionService.updateWorkflowVersionPositions({
workflowVersionId,
positions,
workspaceId,
});
}
if (parameters.edges && parameters.edges.length > 0) {
for (const edge of parameters.edges) {
await this.workflowVersionEdgeService.createWorkflowVersionEdge({
source: edge.source === 'trigger' ? 'trigger' : edge.source,
target: edge.target,
workflowVersionId,
workspaceId,
});
}
}
if (parameters.activate) {
await this.workflowTriggerService.activateWorkflowVersion(
workflowVersionId,
);
await this.updateWorkflowStatus({
workspaceId,
workflowId,
workflowVersionId,
rolePermissionConfig,
});
}
return {
success: true,
message: `Workflow "${parameters.name}" created successfully with ${parameters.steps.length} steps`,
result: {
workflowId,
workflowVersionId,
name: parameters.name,
trigger: parameters.trigger,
steps: parameters.steps,
},
};
} catch (error) {
return {
success: false,
message: `Failed to create workflow "${parameters.name}": ${error.message}`,
error: error.message,
};
}
},
return {
[createCompleteWorkflow.name]: createCompleteWorkflow,
[createWorkflowVersionStep.name]: createWorkflowVersionStep,
[updateWorkflowVersionStep.name]: updateWorkflowVersionStep,
[deleteWorkflowVersionStep.name]: deleteWorkflowVersionStep,
[createWorkflowVersionEdge.name]: createWorkflowVersionEdge,
[deleteWorkflowVersionEdge.name]: deleteWorkflowVersionEdge,
[createDraftFromWorkflowVersion.name]: createDraftFromWorkflowVersion,
[updateWorkflowVersionPositions.name]: updateWorkflowVersionPositions,
[activateWorkflowVersion.name]: activateWorkflowVersion,
[deactivateWorkflowVersion.name]: deactivateWorkflowVersion,
[computeStepOutputSchema.name]: computeStepOutputSchema,
};
tools.create_workflow_version_step = {
description:
'Create a new step in a workflow version. This adds a step to the specified workflow version with the given configuration.',
inputSchema: createWorkflowVersionStepSchema,
execute: async (parameters: CreateWorkflowVersionStepInput) => {
try {
return await this.workflowVersionStepService.createWorkflowVersionStep(
{
workspaceId,
input: parameters,
},
);
} catch (error) {
return {
success: false,
error: error.message,
message: `Failed to create workflow version step: ${error.message}`,
};
}
},
};
tools.update_workflow_version_step = {
description:
'Update an existing step in a workflow version. This modifies the step configuration.',
inputSchema: updateWorkflowVersionStepSchema,
execute: async (parameters: UpdateWorkflowVersionStepInput) => {
try {
return await this.workflowVersionStepService.updateWorkflowVersionStep(
{
workspaceId,
workflowVersionId: parameters.workflowVersionId,
step: parameters.step,
},
);
} catch (error) {
return {
success: false,
error: error.message,
message: `Failed to update workflow version step: ${error.message}`,
};
}
},
};
tools.delete_workflow_version_step = {
description:
'Delete a step from a workflow version. This removes the step and updates the workflow structure.',
inputSchema: deleteWorkflowVersionStepSchema,
execute: async (parameters: {
workflowVersionId: string;
stepId: string;
}) => {
try {
return await this.workflowVersionStepService.deleteWorkflowVersionStep(
{
workspaceId,
workflowVersionId: parameters.workflowVersionId,
stepIdToDelete: parameters.stepId,
},
);
} catch (error) {
return {
success: false,
error: error.message,
message: `Failed to delete workflow version step: ${error.message}`,
};
}
},
};
tools.create_workflow_version_edge = {
description:
'Create a connection (edge) between two workflow steps. This defines the flow between steps.',
inputSchema: createWorkflowVersionEdgeSchema,
execute: async (parameters: {
workflowVersionId: string;
source: string;
target: string;
}) => {
try {
return await this.workflowVersionEdgeService.createWorkflowVersionEdge(
{
source: parameters.source,
target: parameters.target,
workflowVersionId: parameters.workflowVersionId,
workspaceId,
},
);
} catch (error) {
return {
success: false,
error: error.message,
message: `Failed to create workflow version edge: ${error.message}`,
};
}
},
};
tools.delete_workflow_version_edge = {
description: 'Delete a connection (edge) between workflow steps.',
inputSchema: deleteWorkflowVersionEdgeSchema,
execute: async (parameters: {
workflowVersionId: string;
source: string;
target: string;
}) => {
try {
return await this.workflowVersionEdgeService.deleteWorkflowVersionEdge(
{
source: parameters.source,
target: parameters.target,
workflowVersionId: parameters.workflowVersionId,
workspaceId,
},
);
} catch (error) {
return {
success: false,
error: error.message,
message: `Failed to delete workflow version edge: ${error.message}`,
};
}
},
};
tools.create_draft_from_workflow_version = {
description:
'Create a new draft workflow version from an existing one. This allows for iterative workflow development.',
inputSchema: createDraftFromWorkflowVersionSchema,
execute: async (parameters: {
workflowId: string;
workflowVersionIdToCopy: string;
}) => {
try {
return await this.workflowVersionService.createDraftFromWorkflowVersion(
{
workspaceId,
workflowId: parameters.workflowId,
workflowVersionIdToCopy: parameters.workflowVersionIdToCopy,
},
);
} catch (error) {
return {
success: false,
error: error.message,
message: `Failed to create draft from workflow version: ${error.message}`,
};
}
},
};
tools.update_workflow_version_positions = {
description:
'Update the positions of multiple workflow steps. This is useful for reorganizing the workflow layout.',
inputSchema: updateWorkflowVersionPositionsSchema,
execute: async (parameters: UpdateWorkflowVersionPositionsInput) => {
try {
return await this.workflowVersionService.updateWorkflowVersionPositions(
{
workflowVersionId: parameters.workflowVersionId,
positions: parameters.positions,
workspaceId,
},
);
} catch (error) {
return {
success: false,
error: error.message,
message: `Failed to update workflow version step positions: ${error.message}`,
};
}
},
};
tools.activate_workflow_version = {
description:
'Activate a workflow version. This makes the workflow version active and available for execution.',
inputSchema: activateWorkflowVersionSchema,
execute: async (parameters: { workflowVersionId: string }) => {
try {
return await this.workflowTriggerService.activateWorkflowVersion(
parameters.workflowVersionId,
);
} catch (error) {
return {
success: false,
error: error.message,
message: `Failed to activate workflow version: ${error.message}`,
};
}
},
};
tools.deactivate_workflow_version = {
description:
'Deactivate a workflow version. This makes the workflow version inactive and unavailable for execution.',
inputSchema: deactivateWorkflowVersionSchema,
execute: async (parameters: { workflowVersionId: string }) => {
try {
return await this.workflowTriggerService.deactivateWorkflowVersion(
parameters.workflowVersionId,
);
} catch (error) {
return {
success: false,
error: error.message,
message: `Failed to deactivate workflow version: ${error.message}`,
};
}
},
};
tools.compute_step_output_schema = {
description:
'Compute the output schema for a workflow step. This determines what data the step produces. The step parameter must be a valid WorkflowTrigger or WorkflowAction with the correct settings structure for its type.',
inputSchema: computeStepOutputSchemaSchema,
execute: async (parameters: {
step: WorkflowTrigger | WorkflowAction;
workflowVersionId: string;
}) => {
try {
return await this.workflowSchemaService.computeStepOutputSchema({
step: parameters.step,
workspaceId,
workflowVersionId: parameters.workflowVersionId,
});
} catch (error) {
return {
success: false,
error: error.message,
message: `Failed to compute step output schema: ${error.message}`,
};
}
},
};
return tools;
}
private async createWorkflow({
workspaceId,
name,
rolePermissionConfig,
}: {
workspaceId: string;
name: string;
rolePermissionConfig: RolePermissionConfig;
}): Promise<string> {
const workflowRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
// Generates dynamic step configurator tools for each workspace object
async generateRecordStepConfiguratorTools(
workspaceId: string,
rolePermissionConfig: RolePermissionConfig,
toolHints?: ToolHints,
): Promise<ToolSet> {
const workflowStepToolsFactory = createWorkflowStepToolsFactory(
this.workflowStepToolsDeps,
);
return this.perObjectToolGenerator.generate(
{
workspaceId,
'workflow',
rolePermissionConfig,
);
const workflowPosition =
await this.recordPositionService.buildRecordPosition({
value: 'first',
objectMetadata: {
isCustom: false,
nameSingular: 'workflow',
},
workspaceId,
});
const workflow = {
id: uuidv4(),
name,
statuses: [WorkflowStatus.DRAFT],
position: workflowPosition,
};
await workflowRepository.insert(workflow);
return workflow.id;
}
private async createWorkflowVersion({
workspaceId,
workflowId,
trigger,
steps,
rolePermissionConfig,
}: {
workspaceId: string;
workflowId: string;
trigger: WorkflowTrigger;
steps: WorkflowAction[];
rolePermissionConfig: RolePermissionConfig;
}): Promise<string> {
const workflowVersionRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
workspaceId,
'workflowVersion',
rolePermissionConfig,
);
const versionPosition =
await this.recordPositionService.buildRecordPosition({
value: 'first',
objectMetadata: {
isCustom: false,
nameSingular: 'workflowVersion',
},
workspaceId,
});
const workflowVersion = {
id: uuidv4(),
workflowId,
name: 'v1',
status: WorkflowVersionStatus.DRAFT,
trigger,
steps,
position: versionPosition,
};
await workflowVersionRepository.insert(workflowVersion);
return workflowVersion.id;
}
private async updateWorkflowStatus({
workspaceId,
workflowId,
workflowVersionId,
rolePermissionConfig,
}: {
workspaceId: string;
workflowId: string;
workflowVersionId: string;
rolePermissionConfig: RolePermissionConfig;
}) {
const workflowRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
workspaceId,
'workflow',
rolePermissionConfig,
);
await workflowRepository.update(workflowId, {
statuses: [WorkflowStatus.ACTIVE],
lastPublishedVersionId: workflowVersionId,
});
},
[workflowStepToolsFactory],
toolHints,
);
}
}
@@ -0,0 +1,35 @@
import { z } from 'zod';
import { type WorkflowToolDependencies } from 'src/modules/workflow/workflow-tools/types/workflow-tool-dependencies.type';
const activateWorkflowVersionSchema = z.object({
workflowVersionId: z
.string()
.describe('The ID of the workflow version to activate'),
});
type ActivateWorkflowVersionInput = z.infer<
typeof activateWorkflowVersionSchema
>;
export const createActivateWorkflowVersionTool = (
deps: Pick<WorkflowToolDependencies, 'workflowTriggerService'>,
) => ({
name: 'activate_workflow_version' as const,
description:
'Activate a workflow version. This makes the workflow version active and available for execution.',
inputSchema: activateWorkflowVersionSchema,
execute: async (parameters: ActivateWorkflowVersionInput) => {
try {
return await deps.workflowTriggerService.activateWorkflowVersion(
parameters.workflowVersionId,
);
} catch (error) {
return {
success: false,
error: error.message,
message: `Failed to activate workflow version: ${error.message}`,
};
}
},
});
@@ -0,0 +1,47 @@
import {
workflowActionSchema,
workflowTriggerSchema,
} from 'twenty-shared/workflow';
import { z } from 'zod';
import { type WorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
import {
type WorkflowToolContext,
type WorkflowToolDependencies,
} from 'src/modules/workflow/workflow-tools/types/workflow-tool-dependencies.type';
import { type WorkflowTrigger } from 'src/modules/workflow/workflow-trigger/types/workflow-trigger.type';
const computeStepOutputSchemaSchema = z.object({
step: z
.union([workflowTriggerSchema, workflowActionSchema])
.describe('The workflow step configuration'),
workflowVersionId: z.string().describe('The ID of the workflow version'),
});
export const createComputeStepOutputSchemaTool = (
deps: Pick<WorkflowToolDependencies, 'workflowSchemaService'>,
context: WorkflowToolContext,
) => ({
name: 'compute_step_output_schema' as const,
description:
'Compute the output schema for a workflow step. This determines what data the step produces. The step parameter must be a valid WorkflowTrigger or WorkflowAction with the correct settings structure for its type.',
inputSchema: computeStepOutputSchemaSchema,
execute: async (parameters: {
step: WorkflowTrigger | WorkflowAction;
workflowVersionId: string;
}) => {
try {
return await deps.workflowSchemaService.computeStepOutputSchema({
step: parameters.step,
workspaceId: context.workspaceId,
workflowVersionId: parameters.workflowVersionId,
});
} catch (error) {
return {
success: false,
error: error.message,
message: `Failed to compute step output schema: ${error.message}`,
};
}
},
});
@@ -0,0 +1,294 @@
import {
workflowActionSchema,
workflowTriggerSchema,
} from 'twenty-shared/workflow';
import { v4 as uuidv4 } from 'uuid';
import { z } from 'zod';
import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config';
import { WorkflowVersionStatus } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity';
import { WorkflowStatus } from 'src/modules/workflow/common/standard-objects/workflow.workspace-entity';
import { type WorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
import {
type WorkflowToolContext,
type WorkflowToolDependencies,
} from 'src/modules/workflow/workflow-tools/types/workflow-tool-dependencies.type';
import { type WorkflowTrigger } from 'src/modules/workflow/workflow-trigger/types/workflow-trigger.type';
const createCompleteWorkflowSchema = z.object({
name: z.string().describe('The name of the workflow'),
description: z
.string()
.optional()
.describe('Optional description of the workflow'),
trigger: workflowTriggerSchema,
steps: z
.array(workflowActionSchema)
.describe('Array of workflow action steps'),
stepPositions: z
.array(
z.object({
stepId: z
.string()
.describe('The ID of the step (use "trigger" for trigger step)'),
position: z.object({
x: z.number().describe('X coordinate for the step position'),
y: z.number().describe('Y coordinate for the step position'),
}),
}),
)
.optional()
.describe('Optional array of step positions for layout'),
edges: z
.array(
z.object({
source: z
.string()
.describe(
'The ID of the source step (use "trigger" for trigger step)',
),
target: z.string().describe('The ID of the target step'),
}),
)
.optional()
.describe('Optional array of connections between steps'),
activate: z
.boolean()
.optional()
.describe('Whether to activate the workflow immediately (default: false)'),
});
type CreateCompleteWorkflowToolDeps = Pick<
WorkflowToolDependencies,
| 'workflowVersionService'
| 'workflowVersionEdgeService'
| 'workflowTriggerService'
| 'twentyORMGlobalManager'
| 'recordPositionService'
>;
type CreateCompleteWorkflowToolContext = WorkflowToolContext & {
rolePermissionConfig: RolePermissionConfig;
};
export const createCreateCompleteWorkflowTool = (
deps: CreateCompleteWorkflowToolDeps,
context: CreateCompleteWorkflowToolContext,
) => ({
name: 'create_complete_workflow' as const,
description: `Create a complete workflow with trigger, steps, and connections in a single operation.
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
- 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
Common mistakes to avoid:
- Using "RECORD_CREATED" instead of "DATABASE_EVENT"
- Missing the "name" and "valid" fields in steps
- Missing the "objectRecord" field in CREATE_RECORD actions
- Using "fieldsToUpdate" instead of "objectRecord" in CREATE_RECORD actions
IMPORTANT: The tool schema provides comprehensive field descriptions, examples, and validation rules. Always refer to the schema for:
- Field requirements and data types
- Common object patterns and field structures
- Proper relationship field formats
- Variable reference syntax (e.g., {{trigger.object.fieldName}})
- Error handling options
This is the most efficient way for AI to create workflows as it handles all the complexity in one call.`,
inputSchema: createCompleteWorkflowSchema,
execute: async (parameters: {
name: string;
description?: string;
trigger: WorkflowTrigger;
steps: WorkflowAction[];
stepPositions?: Array<{
stepId: string;
position: { x: number; y: number };
}>;
edges?: Array<{ source: string; target: string }>;
activate?: boolean;
}) => {
try {
const workflowId = await createWorkflow({
deps,
context,
name: parameters.name,
});
const workflowVersionId = await createWorkflowVersion({
deps,
context,
workflowId,
trigger: parameters.trigger,
steps: parameters.steps,
});
if (parameters.stepPositions && parameters.stepPositions.length > 0) {
const positions = parameters.stepPositions.map((pos) => ({
id: pos.stepId === 'trigger' ? 'trigger' : pos.stepId,
position: pos.position,
}));
await deps.workflowVersionService.updateWorkflowVersionPositions({
workflowVersionId,
positions,
workspaceId: context.workspaceId,
});
}
if (parameters.edges && parameters.edges.length > 0) {
for (const edge of parameters.edges) {
await deps.workflowVersionEdgeService.createWorkflowVersionEdge({
source: edge.source === 'trigger' ? 'trigger' : edge.source,
target: edge.target,
workflowVersionId,
workspaceId: context.workspaceId,
});
}
}
if (parameters.activate) {
await deps.workflowTriggerService.activateWorkflowVersion(
workflowVersionId,
);
await updateWorkflowStatus({
deps,
context,
workflowId,
workflowVersionId,
});
}
return {
success: true,
message: `Workflow "${parameters.name}" created successfully with ${parameters.steps.length} steps`,
result: {
workflowId,
workflowVersionId,
name: parameters.name,
trigger: parameters.trigger,
steps: parameters.steps,
},
};
} catch (error) {
return {
success: false,
message: `Failed to create workflow "${parameters.name}": ${error.message}`,
error: error.message,
};
}
},
});
const createWorkflow = async ({
deps,
context,
name,
}: {
deps: CreateCompleteWorkflowToolDeps;
context: CreateCompleteWorkflowToolContext;
name: string;
}): Promise<string> => {
const workflowRepository =
await deps.twentyORMGlobalManager.getRepositoryForWorkspace(
context.workspaceId,
'workflow',
context.rolePermissionConfig,
);
const workflowPosition = await deps.recordPositionService.buildRecordPosition(
{
value: 'first',
objectMetadata: {
isCustom: false,
nameSingular: 'workflow',
},
workspaceId: context.workspaceId,
},
);
const workflow = {
id: uuidv4(),
name,
statuses: [WorkflowStatus.DRAFT],
position: workflowPosition,
};
await workflowRepository.insert(workflow);
return workflow.id;
};
const createWorkflowVersion = async ({
deps,
context,
workflowId,
trigger,
steps,
}: {
deps: CreateCompleteWorkflowToolDeps;
context: CreateCompleteWorkflowToolContext;
workflowId: string;
trigger: WorkflowTrigger;
steps: WorkflowAction[];
}): Promise<string> => {
const workflowVersionRepository =
await deps.twentyORMGlobalManager.getRepositoryForWorkspace(
context.workspaceId,
'workflowVersion',
context.rolePermissionConfig,
);
const versionPosition = await deps.recordPositionService.buildRecordPosition({
value: 'first',
objectMetadata: {
isCustom: false,
nameSingular: 'workflowVersion',
},
workspaceId: context.workspaceId,
});
const workflowVersion = {
id: uuidv4(),
workflowId,
name: 'v1',
status: WorkflowVersionStatus.DRAFT,
trigger,
steps,
position: versionPosition,
};
await workflowVersionRepository.insert(workflowVersion);
return workflowVersion.id;
};
const updateWorkflowStatus = async ({
deps,
context,
workflowId,
workflowVersionId,
}: {
deps: CreateCompleteWorkflowToolDeps;
context: CreateCompleteWorkflowToolContext;
workflowId: string;
workflowVersionId: string;
}) => {
const workflowRepository =
await deps.twentyORMGlobalManager.getRepositoryForWorkspace(
context.workspaceId,
'workflow',
context.rolePermissionConfig,
);
await workflowRepository.update(workflowId, {
statuses: [WorkflowStatus.ACTIVE],
lastPublishedVersionId: workflowVersionId,
});
};
@@ -0,0 +1,42 @@
import { z } from 'zod';
import {
type WorkflowToolContext,
type WorkflowToolDependencies,
} from 'src/modules/workflow/workflow-tools/types/workflow-tool-dependencies.type';
const createDraftFromWorkflowVersionSchema = z.object({
workflowId: z.string().describe('The ID of the workflow'),
workflowVersionIdToCopy: z
.string()
.describe('The ID of the workflow version to create a draft from'),
});
type CreateDraftFromWorkflowVersionInput = z.infer<
typeof createDraftFromWorkflowVersionSchema
>;
export const createCreateDraftFromWorkflowVersionTool = (
deps: Pick<WorkflowToolDependencies, 'workflowVersionService'>,
context: WorkflowToolContext,
) => ({
name: 'create_draft_from_workflow_version' as const,
description:
'Create a new draft workflow version from an existing one. This allows for iterative workflow development.',
inputSchema: createDraftFromWorkflowVersionSchema,
execute: async (parameters: CreateDraftFromWorkflowVersionInput) => {
try {
return await deps.workflowVersionService.createDraftFromWorkflowVersion({
workspaceId: context.workspaceId,
workflowId: parameters.workflowId,
workflowVersionIdToCopy: parameters.workflowVersionIdToCopy,
});
} catch (error) {
return {
success: false,
error: error.message,
message: `Failed to create draft from workflow version: ${error.message}`,
};
}
},
});
@@ -0,0 +1,53 @@
import { z } from 'zod';
import { WorkflowActionType } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action-type.enum';
import {
type WorkflowToolContext,
type WorkflowToolDependencies,
} from 'src/modules/workflow/workflow-tools/types/workflow-tool-dependencies.type';
const createWorkflowVersionEdgeSchema = z.object({
workflowVersionId: z.string().describe('The ID of the workflow version'),
source: z.string().describe('The ID of the source step'),
target: z.string().describe('The ID of the target step'),
sourceConnectionOptions: z
.object({
connectedStepType: z.literal(WorkflowActionType.ITERATOR),
settings: z.object({
isConnectedToLoop: z.boolean(),
}),
})
.optional()
.describe('Optional connection options for iterator steps'),
});
type CreateWorkflowVersionEdgeInput = z.infer<
typeof createWorkflowVersionEdgeSchema
>;
export const createCreateWorkflowVersionEdgeTool = (
deps: Pick<WorkflowToolDependencies, 'workflowVersionEdgeService'>,
context: WorkflowToolContext,
) => ({
name: 'create_workflow_version_edge' as const,
description:
'Create a connection (edge) between two workflow steps. This defines the flow between steps.',
inputSchema: createWorkflowVersionEdgeSchema,
execute: async (parameters: CreateWorkflowVersionEdgeInput) => {
try {
return await deps.workflowVersionEdgeService.createWorkflowVersionEdge({
source: parameters.source,
target: parameters.target,
workflowVersionId: parameters.workflowVersionId,
workspaceId: context.workspaceId,
sourceConnectionOptions: parameters.sourceConnectionOptions,
});
} catch (error) {
return {
success: false,
error: error.message,
message: `Failed to create workflow version edge: ${error.message}`,
};
}
},
});
@@ -0,0 +1,64 @@
import { z } from 'zod';
import type { CreateWorkflowVersionStepInput } from 'src/engine/core-modules/workflow/dtos/create-workflow-version-step-input.dto';
import { WorkflowActionType } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action-type.enum';
import {
type WorkflowToolContext,
type WorkflowToolDependencies,
} from 'src/modules/workflow/workflow-tools/types/workflow-tool-dependencies.type';
const createWorkflowVersionStepSchema = z.object({
workflowVersionId: z
.string()
.describe('The ID of the workflow version to add the step to'),
stepType: z
.enum(Object.values(WorkflowActionType) as [string, ...string[]])
.describe('The type of step to create'),
parentStepId: z
.string()
.optional()
.describe('Optional ID of the parent step this step should come after'),
parentStepConnectionOptions: z
.object({
type: z.string().optional(),
conditionGroupIndex: z.number().optional(),
})
.optional()
.describe('Optional parent step connection options'),
nextStepId: z
.string()
.optional()
.describe('Optional ID of the step this new step should connect to'),
position: z
.object({
x: z.number(),
y: z.number(),
})
.optional()
.describe('Optional position coordinates for the step'),
id: z.string().optional().describe('Optional step ID'),
});
export const createCreateWorkflowVersionStepTool = (
deps: Pick<WorkflowToolDependencies, 'workflowVersionStepService'>,
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.',
inputSchema: createWorkflowVersionStepSchema,
execute: async (parameters: CreateWorkflowVersionStepInput) => {
try {
return await deps.workflowVersionStepService.createWorkflowVersionStep({
workspaceId: context.workspaceId,
input: parameters,
});
} catch (error) {
return {
success: false,
error: error.message,
message: `Failed to create workflow version step: ${error.message}`,
};
}
},
});
@@ -0,0 +1,35 @@
import { z } from 'zod';
import { type WorkflowToolDependencies } from 'src/modules/workflow/workflow-tools/types/workflow-tool-dependencies.type';
const deactivateWorkflowVersionSchema = z.object({
workflowVersionId: z
.string()
.describe('The ID of the workflow version to deactivate'),
});
type DeactivateWorkflowVersionInput = z.infer<
typeof deactivateWorkflowVersionSchema
>;
export const createDeactivateWorkflowVersionTool = (
deps: Pick<WorkflowToolDependencies, 'workflowTriggerService'>,
) => ({
name: 'deactivate_workflow_version' as const,
description:
'Deactivate a workflow version. This makes the workflow version inactive and unavailable for execution.',
inputSchema: deactivateWorkflowVersionSchema,
execute: async (parameters: DeactivateWorkflowVersionInput) => {
try {
return await deps.workflowTriggerService.deactivateWorkflowVersion(
parameters.workflowVersionId,
);
} catch (error) {
return {
success: false,
error: error.message,
message: `Failed to deactivate workflow version: ${error.message}`,
};
}
},
});
@@ -0,0 +1,52 @@
import { z } from 'zod';
import { WorkflowActionType } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action-type.enum';
import {
type WorkflowToolContext,
type WorkflowToolDependencies,
} from 'src/modules/workflow/workflow-tools/types/workflow-tool-dependencies.type';
const deleteWorkflowVersionEdgeSchema = z.object({
workflowVersionId: z.string().describe('The ID of the workflow version'),
source: z.string().describe('The ID of the source step'),
target: z.string().describe('The ID of the target step'),
sourceConnectionOptions: z
.object({
connectedStepType: z.literal(WorkflowActionType.ITERATOR),
settings: z.object({
isConnectedToLoop: z.boolean(),
}),
})
.optional()
.describe('Optional connection options for iterator steps'),
});
type DeleteWorkflowVersionEdgeInput = z.infer<
typeof deleteWorkflowVersionEdgeSchema
>;
export const createDeleteWorkflowVersionEdgeTool = (
deps: Pick<WorkflowToolDependencies, 'workflowVersionEdgeService'>,
context: WorkflowToolContext,
) => ({
name: 'delete_workflow_version_edge' as const,
description: 'Delete a connection (edge) between workflow steps.',
inputSchema: deleteWorkflowVersionEdgeSchema,
execute: async (parameters: DeleteWorkflowVersionEdgeInput) => {
try {
return await deps.workflowVersionEdgeService.deleteWorkflowVersionEdge({
source: parameters.source,
target: parameters.target,
workflowVersionId: parameters.workflowVersionId,
workspaceId: context.workspaceId,
sourceConnectionOptions: parameters.sourceConnectionOptions,
});
} catch (error) {
return {
success: false,
error: error.message,
message: `Failed to delete workflow version edge: ${error.message}`,
};
}
},
});
@@ -0,0 +1,42 @@
import { z } from 'zod';
import {
type WorkflowToolContext,
type WorkflowToolDependencies,
} from 'src/modules/workflow/workflow-tools/types/workflow-tool-dependencies.type';
const deleteWorkflowVersionStepSchema = z.object({
workflowVersionId: z
.string()
.describe('The ID of the workflow version containing the step'),
stepId: z.string().describe('The ID of the step to delete'),
});
type DeleteWorkflowVersionStepInput = z.infer<
typeof deleteWorkflowVersionStepSchema
>;
export const createDeleteWorkflowVersionStepTool = (
deps: Pick<WorkflowToolDependencies, 'workflowVersionStepService'>,
context: WorkflowToolContext,
) => ({
name: 'delete_workflow_version_step' as const,
description:
'Delete a step from a workflow version. This removes the step and updates the workflow structure.',
inputSchema: deleteWorkflowVersionStepSchema,
execute: async (parameters: DeleteWorkflowVersionStepInput) => {
try {
return await deps.workflowVersionStepService.deleteWorkflowVersionStep({
workspaceId: context.workspaceId,
workflowVersionId: parameters.workflowVersionId,
stepIdToDelete: parameters.stepId,
});
} catch (error) {
return {
success: false,
error: error.message,
message: `Failed to delete workflow version step: ${error.message}`,
};
}
},
});
@@ -0,0 +1,47 @@
import { z } from 'zod';
import type { UpdateWorkflowVersionPositionsInput } from 'src/engine/core-modules/workflow/dtos/update-workflow-version-positions-input.dto';
import {
type WorkflowToolContext,
type WorkflowToolDependencies,
} from 'src/modules/workflow/workflow-tools/types/workflow-tool-dependencies.type';
const updateWorkflowVersionPositionsSchema = z.object({
workflowVersionId: z.string().describe('The ID of the workflow version'),
positions: z
.array(
z.object({
id: z.string().describe('Step or trigger ID'),
position: z.object({
x: z.number(),
y: z.number(),
}),
}),
)
.describe('Array of step positions to update'),
});
export const createUpdateWorkflowVersionPositionsTool = (
deps: Pick<WorkflowToolDependencies, 'workflowVersionService'>,
context: WorkflowToolContext,
) => ({
name: 'update_workflow_version_positions' as const,
description:
'Update the positions of multiple workflow steps. This is useful for reorganizing the workflow layout.',
inputSchema: updateWorkflowVersionPositionsSchema,
execute: async (parameters: UpdateWorkflowVersionPositionsInput) => {
try {
return await deps.workflowVersionService.updateWorkflowVersionPositions({
workflowVersionId: parameters.workflowVersionId,
positions: parameters.positions,
workspaceId: context.workspaceId,
});
} catch (error) {
return {
success: false,
error: error.message,
message: `Failed to update workflow version step positions: ${error.message}`,
};
}
},
});
@@ -0,0 +1,45 @@
import {
workflowActionSchema,
workflowTriggerSchema,
} from 'twenty-shared/workflow';
import { z } from 'zod';
import type { UpdateWorkflowVersionStepInput } from 'src/engine/core-modules/workflow/dtos/update-workflow-version-step-input.dto';
import {
type WorkflowToolContext,
type WorkflowToolDependencies,
} from 'src/modules/workflow/workflow-tools/types/workflow-tool-dependencies.type';
const updateWorkflowVersionStepSchema = z.object({
workflowVersionId: z
.string()
.describe('The ID of the workflow version containing the step'),
step: z
.union([workflowTriggerSchema, workflowActionSchema])
.describe('The updated step configuration'),
});
export const createUpdateWorkflowVersionStepTool = (
deps: Pick<WorkflowToolDependencies, 'workflowVersionStepService'>,
context: WorkflowToolContext,
) => ({
name: 'update_workflow_version_step' as const,
description:
'Update an existing step in a workflow version. This modifies the step configuration.',
inputSchema: updateWorkflowVersionStepSchema,
execute: async (parameters: UpdateWorkflowVersionStepInput) => {
try {
return await deps.workflowVersionStepService.updateWorkflowVersionStep({
workspaceId: context.workspaceId,
workflowVersionId: parameters.workflowVersionId,
step: parameters.step,
});
} catch (error) {
return {
success: false,
error: error.message,
message: `Failed to update workflow version step: ${error.message}`,
};
}
},
});
@@ -0,0 +1,21 @@
import type { RecordPositionService } from 'src/engine/core-modules/record-position/services/record-position.service';
import type { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.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 { 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;
workflowVersionEdgeService: WorkflowVersionEdgeWorkspaceService;
workflowVersionService: WorkflowVersionWorkspaceService;
workflowTriggerService: WorkflowTriggerWorkspaceService;
workflowSchemaService: WorkflowSchemaWorkspaceService;
twentyORMGlobalManager: TwentyORMGlobalManager;
recordPositionService: RecordPositionService;
};
export type WorkflowToolContext = {
workspaceId: string;
};
@@ -1,6 +1,7 @@
import { Module } from '@nestjs/common';
import { RecordPositionModule } from 'src/engine/core-modules/record-position/record-position.module';
import { ToolGeneratorModule } from 'src/engine/core-modules/tool-generator/tool-generator.module';
import { WorkflowSchemaModule } from 'src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.module';
import { WorkflowVersionEdgeModule } from 'src/modules/workflow/workflow-builder/workflow-version-edge/workflow-version-edge.module';
import { WorkflowVersionStepModule } from 'src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step.module';
@@ -17,6 +18,7 @@ import { WorkflowToolWorkspaceService } from './services/workflow-tool.workspace
WorkflowTriggerModule,
WorkflowSchemaModule,
RecordPositionModule,
ToolGeneratorModule,
],
providers: [WorkflowToolWorkspaceService],
exports: [WorkflowToolWorkspaceService],