feat(ai) - add delete_workflow tool (#22432)
## Summary - Add a new `delete_workflow` agent tool that soft-deletes a workflow and cleans up its sub-entities (versions, runs, triggers) via `WorkflowCommonWorkspaceService.handleWorkflowSubEntities` - Update the workflow skill system prompt to document the new capability and instruct the agent to always confirm with the user before deleting - Wire `WorkflowCommonModule` / `WorkflowCommonWorkspaceService` into the workflow-tools dependency graph ## Test plan - [x] Unit tests added (`delete-workflow.tool.spec.ts`) covering successful deletion and error handling - [ ] Verify the agent can resolve a workflow by name via `list_workflows` then delete it with `delete_workflow` - [ ] Confirm the agent asks for user confirmation before executing the deletion - [ ] Confirm sub-entities (versions, runs, triggers) are removed after deletion <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22432?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. -->
This commit is contained in:
+9
@@ -24,6 +24,7 @@ You help users create and manage automation workflows.
|
||||
|
||||
- Create workflows from scratch
|
||||
- Modify existing workflows (add, remove, update steps)
|
||||
- Delete workflows entirely (with their versions, runs and triggers) - IMPORTANT : Always confirm with the user before deleting
|
||||
- Explain workflow structure and suggest improvements
|
||||
- Troubleshoot workflow runs (inspect status, failed steps, and execution logs)
|
||||
|
||||
@@ -72,6 +73,14 @@ LOGIC_FUNCTION steps execute logic functions provided by installed applications.
|
||||
|
||||
To discover existing workflows in the workspace, use \`list_workflows\`. Use this before modifying a workflow when the user refers to it by name rather than id — resolve the \`id\` here first, then call \`get_workflow_current_version\` with it.
|
||||
|
||||
## Deleting Workflows
|
||||
|
||||
To delete a workflow entirely, use \`delete_workflow\` with its \`workflowId\`. This also removes the workflow's versions, runs and automated triggers, and deactivates any active version — it is a destructive, irreversible operation.
|
||||
|
||||
- If the user refers to the workflow by name, resolve its \`workflowId\` with \`list_workflows\` first.
|
||||
- IMPORTANT : Always confirm with the user before deleting, and make sure you are deleting the correct workflow.
|
||||
- To simply stop a workflow from running without removing it, prefer \`deactivate_workflow_version\` instead of deleting.
|
||||
|
||||
## Troubleshooting Workflow Runs
|
||||
|
||||
When a user reports a failing or misbehaving workflow, diagnose it with two read-only tools:
|
||||
|
||||
+9
@@ -8,6 +8,7 @@ import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadat
|
||||
import { LogicFunctionFromSourceService } from 'src/engine/metadata-modules/logic-function/services/logic-function-from-source.service';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config';
|
||||
import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service';
|
||||
import { WorkflowSchemaWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.workspace-service';
|
||||
import { WorkflowValidationWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-validation/workflow-validation.workspace-service';
|
||||
import { WorkflowVersionEdgeWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-version-edge/workflow-version-edge.workspace-service';
|
||||
@@ -21,6 +22,7 @@ import { createCreateDraftFromWorkflowVersionTool } from 'src/modules/workflow/w
|
||||
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 { createDeleteWorkflowTool } from 'src/modules/workflow/workflow-tools/tools/delete-workflow.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 { createGetWorkflowCurrentVersionTool } from 'src/modules/workflow/workflow-tools/tools/get-workflow-current-version.tool';
|
||||
@@ -54,6 +56,7 @@ export class WorkflowToolWorkspaceService {
|
||||
logicFunctionFromSourceService: LogicFunctionFromSourceService,
|
||||
flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
agentService: AgentService,
|
||||
workflowCommonService: WorkflowCommonWorkspaceService,
|
||||
) {
|
||||
this.deps = {
|
||||
workflowVersionStepService,
|
||||
@@ -68,6 +71,7 @@ export class WorkflowToolWorkspaceService {
|
||||
logicFunctionFromSourceService,
|
||||
flatEntityMapsCacheService,
|
||||
agentService,
|
||||
workflowCommonService,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -139,6 +143,10 @@ export class WorkflowToolWorkspaceService {
|
||||
this.deps,
|
||||
contextWithPermissions,
|
||||
);
|
||||
const deleteWorkflow = createDeleteWorkflowTool(
|
||||
this.deps,
|
||||
contextWithPermissions,
|
||||
);
|
||||
const updateLogicFunctionSource = createUpdateLogicFunctionSourceTool(
|
||||
this.deps,
|
||||
context,
|
||||
@@ -165,6 +173,7 @@ export class WorkflowToolWorkspaceService {
|
||||
[computeStepOutputSchema.name]: computeStepOutputSchema,
|
||||
[getWorkflowCurrentVersion.name]: getWorkflowCurrentVersion,
|
||||
[listWorkflows.name]: listWorkflows,
|
||||
[deleteWorkflow.name]: deleteWorkflow,
|
||||
[getWorkflowRun.name]: getWorkflowRun,
|
||||
[listWorkflowRuns.name]: listWorkflowRuns,
|
||||
[updateLogicFunctionSource.name]: updateLogicFunctionSource,
|
||||
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
import { createDeleteWorkflowTool } from 'src/modules/workflow/workflow-tools/tools/delete-workflow.tool';
|
||||
|
||||
const WORKFLOW_ID = 'b3b8a4f0-0000-4000-8000-000000000000';
|
||||
|
||||
const buildTool = () => {
|
||||
const workflowRepository = {
|
||||
softDelete: jest.fn().mockResolvedValue({ affected: 1 }),
|
||||
};
|
||||
const globalWorkspaceOrmManager = {
|
||||
executeInWorkspaceContext: jest.fn((callback: () => unknown) => callback()),
|
||||
getRepository: jest.fn().mockResolvedValue(workflowRepository),
|
||||
};
|
||||
const workflowCommonService = {
|
||||
handleWorkflowSubEntities: jest.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
|
||||
const tool = createDeleteWorkflowTool(
|
||||
{
|
||||
globalWorkspaceOrmManager,
|
||||
workflowCommonService,
|
||||
} as never,
|
||||
{
|
||||
workspaceId: 'workspace-id',
|
||||
rolePermissionConfig: { shouldBypassPermissionChecks: true },
|
||||
} as never,
|
||||
);
|
||||
|
||||
return {
|
||||
tool,
|
||||
workflowRepository,
|
||||
globalWorkspaceOrmManager,
|
||||
workflowCommonService,
|
||||
};
|
||||
};
|
||||
|
||||
const baseInput = {
|
||||
workflowId: WORKFLOW_ID,
|
||||
} as unknown as Parameters<
|
||||
ReturnType<typeof createDeleteWorkflowTool>['execute']
|
||||
>[0];
|
||||
|
||||
describe('createDeleteWorkflowTool', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should soft delete the workflow and clean up sub-entities', async () => {
|
||||
const { tool, workflowRepository, workflowCommonService } = buildTool();
|
||||
|
||||
const result = (await tool.execute(baseInput)) as Record<string, unknown>;
|
||||
|
||||
expect(workflowRepository.softDelete).toHaveBeenCalledWith(WORKFLOW_ID);
|
||||
expect(
|
||||
workflowCommonService.handleWorkflowSubEntities,
|
||||
).toHaveBeenCalledWith({
|
||||
workflowIds: [WORKFLOW_ID],
|
||||
workspaceId: 'workspace-id',
|
||||
operation: 'delete',
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.workflowId).toBe(WORKFLOW_ID);
|
||||
});
|
||||
|
||||
it('should return a failure result when deletion throws', async () => {
|
||||
const { tool, workflowRepository } = buildTool();
|
||||
|
||||
workflowRepository.softDelete.mockRejectedValue(new Error('boom'));
|
||||
|
||||
const result = (await tool.execute(baseInput)) as Record<string, unknown>;
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toBe('boom');
|
||||
expect(result.message).toContain('boom');
|
||||
});
|
||||
});
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config';
|
||||
import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
|
||||
import { type WorkflowWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow.workspace-entity';
|
||||
import {
|
||||
type WorkflowToolContext,
|
||||
type WorkflowToolDependencies,
|
||||
} from 'src/modules/workflow/workflow-tools/types/workflow-tool-dependencies.type';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type DeleteWorkflowToolContext = WorkflowToolContext & {
|
||||
rolePermissionConfig: RolePermissionConfig;
|
||||
};
|
||||
|
||||
const deleteWorkflowSchema = z.object({
|
||||
workflowId: z.string().uuid().describe('The UUID of the workflow to delete'),
|
||||
});
|
||||
|
||||
type DeleteWorkflowInput = z.infer<typeof deleteWorkflowSchema>;
|
||||
|
||||
export const createDeleteWorkflowTool = (
|
||||
deps: Pick<
|
||||
WorkflowToolDependencies,
|
||||
'globalWorkspaceOrmManager' | 'workflowCommonService'
|
||||
>,
|
||||
context: DeleteWorkflowToolContext,
|
||||
) => ({
|
||||
name: 'delete_workflow' as const,
|
||||
description:
|
||||
'Delete a workflow by its ID. This also removes its versions, runs and automated triggers, and deactivates any active version. Use list_workflows to find the workflowId.',
|
||||
inputSchema: deleteWorkflowSchema,
|
||||
execute: async (parameters: DeleteWorkflowInput) => {
|
||||
try {
|
||||
const { workflowId } = parameters;
|
||||
const { workspaceId } = context;
|
||||
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
|
||||
const deleteResult =
|
||||
await deps.globalWorkspaceOrmManager.executeInWorkspaceContext(
|
||||
async () => {
|
||||
const workflowRepository =
|
||||
await deps.globalWorkspaceOrmManager.getRepository<WorkflowWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'workflow',
|
||||
context.rolePermissionConfig,
|
||||
);
|
||||
|
||||
return workflowRepository.softDelete(workflowId);
|
||||
},
|
||||
authContext,
|
||||
);
|
||||
|
||||
if (!isDefined(deleteResult.affected)) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Workflow not found',
|
||||
message: `No workflow found with ID ${workflowId}`,
|
||||
};
|
||||
}
|
||||
|
||||
await deps.workflowCommonService.handleWorkflowSubEntities({
|
||||
workflowIds: [workflowId],
|
||||
workspaceId,
|
||||
operation: 'delete',
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Successfully deleted workflow ${workflowId}`,
|
||||
workflowId,
|
||||
};
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
message: `Failed to delete workflow: ${errorMessage}`,
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
+2
@@ -3,6 +3,7 @@ import type { AgentService } from 'src/engine/metadata-modules/ai/ai-agent/agent
|
||||
import type { LogicFunctionFromSourceService } from 'src/engine/metadata-modules/logic-function/services/logic-function-from-source.service';
|
||||
import type { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import type { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import type { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service';
|
||||
import type { WorkflowSchemaWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.workspace-service';
|
||||
import type { WorkflowValidationWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-validation/workflow-validation.workspace-service';
|
||||
import type { WorkflowVersionEdgeWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-version-edge/workflow-version-edge.workspace-service';
|
||||
@@ -24,6 +25,7 @@ export type WorkflowToolDependencies = {
|
||||
logicFunctionFromSourceService: LogicFunctionFromSourceService;
|
||||
flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService;
|
||||
agentService: AgentService;
|
||||
workflowCommonService: WorkflowCommonWorkspaceService;
|
||||
};
|
||||
|
||||
export type WorkflowToolContext = {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { WORKFLOW_TOOL_SERVICE_TOKEN } from 'src/engine/core-modules/tool-provid
|
||||
import { AiAgentModule } from 'src/engine/metadata-modules/ai/ai-agent/ai-agent.module';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
|
||||
import { LogicFunctionModule } from 'src/engine/metadata-modules/logic-function/logic-function.module';
|
||||
import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-common.module';
|
||||
import { WorkflowSchemaModule } from 'src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.module';
|
||||
import { WorkflowValidationModule } from 'src/modules/workflow/workflow-builder/workflow-validation/workflow-validation.module';
|
||||
import { WorkflowVersionEdgeModule } from 'src/modules/workflow/workflow-builder/workflow-version-edge/workflow-version-edge.module';
|
||||
@@ -23,6 +24,7 @@ import { WorkflowToolWorkspaceService } from './services/workflow-tool.workspace
|
||||
WorkflowVersionEdgeModule,
|
||||
WorkflowVersionModule,
|
||||
WorkflowTriggerModule,
|
||||
WorkflowCommonModule,
|
||||
WorkflowSchemaModule,
|
||||
WorkflowValidationModule,
|
||||
RecordPositionModule,
|
||||
|
||||
Reference in New Issue
Block a user