diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/skill-metadata/create-standard-flat-skill-metadata.util.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/skill-metadata/create-standard-flat-skill-metadata.util.ts index 9ee9e58c8d..dc068e06ba 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/skill-metadata/create-standard-flat-skill-metadata.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/skill-metadata/create-standard-flat-skill-metadata.util.ts @@ -25,6 +25,7 @@ You help users create and manage automation workflows. - Create workflows from scratch - Modify existing workflows (add, remove, update steps) - Explain workflow structure and suggest improvements +- Troubleshoot workflow runs (inspect status, failed steps, and execution logs) ## Key Concepts @@ -67,6 +68,23 @@ LOGIC_FUNCTION steps execute logic functions provided by installed applications. { "stepType": "LOGIC_FUNCTION", "workflowVersionId": "", "defaultSettings": { "input": { "logicFunctionId": "" } } } 3. Or when using \`create_complete_workflow\`, include a step with type "LOGIC_FUNCTION" and settings.input.logicFunctionId. +## Troubleshooting Workflow Runs + +When a user reports a failing or misbehaving workflow, diagnose it with two read-only tools: + +- \`list_workflow_runs\`: lists runs (optional \`workflowId\`, optional \`status\`, optional \`limit\`), most recent first. Each result carries \`id\`, \`name\`, \`status\`, run-level \`error\`, \`startedAt\`, \`endedAt\`, \`workflowId\`, and \`workflowVersionId\`. +- \`get_workflow_run\`: returns full details for one run (\`workflowRunId\`) — overall status, run-level error, every step's status/error, and the execution logs of the steps that failed. + +### Resolving the run when no id is given + +For requests like "fix my latest failed workflow" where no run or workflow id is provided, call \`list_workflow_runs\` with \`status\` "FAILED" and NO \`workflowId\` — this returns the most recent failed run across all workflows, and each result already carries \`workflowId\`, \`workflowVersionId\`, and a human-readable \`name\`, so you never need an id from the user. If the user names a specific workflow, resolve its \`workflowId\` first and pass it as a filter. + +### Flow + +1. Identify the run via \`list_workflow_runs\` (use \`limit\` 5 when no \`workflowId\` so you can detect multiple failing workflows). +2. If results span multiple \`workflowId\`s, disambiguate by name with the user before editing anything. +3. Call \`get_workflow_run\` on the chosen run id to read the failed step(s) and their error/logs. +4. Map back to the workflow definition via \`get_workflow_current_version(workflowId)\`, then propose or apply a fix. ## PICK_RECORD Steps PICK_RECORD selects one record from a candidate pool (settings.input.recordIds) and outputs it for later steps to reference — useful for assignment workflows like picking an owner. Set settings.input.strategy to RANDOM, ROUND_ROBIN, or LOAD_BALANCED; LOAD_BALANCED also needs settings.input.loadBalance.{objectNameSingular, fieldName} to pick the candidate with the fewest related records. 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 9c7c22a258..7d828c7c87 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 @@ -4,8 +4,8 @@ import { type ToolSet } from 'ai'; import { RecordPositionService } from 'src/engine/core-modules/record-position/services/record-position.service'; import { AgentService } from 'src/engine/metadata-modules/ai/ai-agent/agent.service'; -import { LogicFunctionFromSourceService } from 'src/engine/metadata-modules/logic-function/services/logic-function-from-source.service'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; +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 { WorkflowSchemaWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.workspace-service'; @@ -24,7 +24,9 @@ import { createDeactivateWorkflowVersionTool } from 'src/modules/workflow/workfl 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'; +import { createGetWorkflowRunTool } from 'src/modules/workflow/workflow-tools/tools/get-workflow-run.tool'; import { createListLogicFunctionToolsTool } from 'src/modules/workflow/workflow-tools/tools/list-logic-function-tools.tool'; +import { createListWorkflowRunsTool } from 'src/modules/workflow/workflow-tools/tools/list-workflow-runs.tool'; import { createUpdateAgentTool } from 'src/modules/workflow/workflow-tools/tools/update-agent.tool'; import { createUpdateLogicFunctionSourceTool } from 'src/modules/workflow/workflow-tools/tools/update-logic-function-source.tool'; import { createUpdateWorkflowVersionPositionsTool } from 'src/modules/workflow/workflow-tools/tools/update-workflow-version-positions.tool'; @@ -122,7 +124,15 @@ export class WorkflowToolWorkspaceService { ); const getWorkflowCurrentVersion = createGetWorkflowCurrentVersionTool( this.deps, - context, + contextWithPermissions, + ); + const getWorkflowRun = createGetWorkflowRunTool( + this.deps, + contextWithPermissions, + ); + const listWorkflowRuns = createListWorkflowRunsTool( + this.deps, + contextWithPermissions, ); const updateLogicFunctionSource = createUpdateLogicFunctionSourceTool( this.deps, @@ -149,6 +159,8 @@ export class WorkflowToolWorkspaceService { [deactivateWorkflowVersion.name]: deactivateWorkflowVersion, [computeStepOutputSchema.name]: computeStepOutputSchema, [getWorkflowCurrentVersion.name]: getWorkflowCurrentVersion, + [getWorkflowRun.name]: getWorkflowRun, + [listWorkflowRuns.name]: listWorkflowRuns, [updateLogicFunctionSource.name]: updateLogicFunctionSource, [listLogicFunctionTools.name]: listLogicFunctionTools, [updateAgent.name]: updateAgent, diff --git a/packages/twenty-server/src/modules/workflow/workflow-tools/tools/__tests__/get-workflow-current-version.tool.spec.ts b/packages/twenty-server/src/modules/workflow/workflow-tools/tools/__tests__/get-workflow-current-version.tool.spec.ts new file mode 100644 index 0000000000..6aa952d5c5 --- /dev/null +++ b/packages/twenty-server/src/modules/workflow/workflow-tools/tools/__tests__/get-workflow-current-version.tool.spec.ts @@ -0,0 +1,165 @@ +import { WorkflowVersionStatus } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity'; +import { type WorkflowToolDependencies } from 'src/modules/workflow/workflow-tools/types/workflow-tool-dependencies.type'; + +import { createGetWorkflowCurrentVersionTool } from '../get-workflow-current-version.tool'; + +const WORKSPACE_ID = '20202020-aaaa-4d02-bf25-6aeccf7ea419'; +const WORKFLOW_ID = '20202020-bbbb-4d02-bf25-6aeccf7ea419'; +const ROLE_ID = '20202020-cccc-4d02-bf25-6aeccf7ea419'; + +const buildContext = () => ({ + workspaceId: WORKSPACE_ID, + rolePermissionConfig: { intersectionOf: [ROLE_ID] }, +}); + +const buildDeps = ({ + workflow, + versions, +}: { + workflow: unknown; + versions: unknown[]; +}) => { + const getRepositoryMock = jest.fn(); + + getRepositoryMock.mockResolvedValueOnce({ + findOne: jest.fn().mockResolvedValue(workflow), + }); + + getRepositoryMock.mockResolvedValueOnce({ + find: jest.fn().mockResolvedValue(versions), + }); + + return { + globalWorkspaceOrmManager: { + executeInWorkspaceContext: jest + .fn() + .mockImplementation(async (fn) => fn()), + getRepository: getRepositoryMock, + }, + }; +}; + +describe('get_workflow_current_version tool', () => { + it('should pass rolePermissionConfig to both getRepository calls', async () => { + const context = buildContext(); + const deps = buildDeps({ + workflow: { id: WORKFLOW_ID }, + versions: [ + { + id: 'v1', + status: WorkflowVersionStatus.DRAFT, + workflowId: WORKFLOW_ID, + }, + ], + }); + + const tool = createGetWorkflowCurrentVersionTool( + deps as unknown as Pick< + WorkflowToolDependencies, + 'globalWorkspaceOrmManager' + >, + context, + ); + + await tool.execute({ workflowId: WORKFLOW_ID }); + + expect( + deps.globalWorkspaceOrmManager.getRepository, + ).toHaveBeenNthCalledWith( + 1, + WORKSPACE_ID, + 'workflow', + context.rolePermissionConfig, + ); + expect( + deps.globalWorkspaceOrmManager.getRepository, + ).toHaveBeenNthCalledWith( + 2, + WORKSPACE_ID, + 'workflowVersion', + context.rolePermissionConfig, + ); + }); + + it('should return draft version over active version', async () => { + const context = buildContext(); + const deps = buildDeps({ + workflow: { id: WORKFLOW_ID }, + versions: [ + { + id: 'v-active', + name: 'Active', + status: WorkflowVersionStatus.ACTIVE, + workflowId: WORKFLOW_ID, + trigger: null, + steps: [], + }, + { + id: 'v-draft', + name: 'Draft', + status: WorkflowVersionStatus.DRAFT, + workflowId: WORKFLOW_ID, + trigger: null, + steps: [], + }, + ], + }); + + const tool = createGetWorkflowCurrentVersionTool( + deps as unknown as Pick< + WorkflowToolDependencies, + 'globalWorkspaceOrmManager' + >, + context, + ); + + const result = await tool.execute({ workflowId: WORKFLOW_ID }); + + expect(result.success).toBe(true); + + if ( + !('workflowVersion' in result) || + result.workflowVersion === undefined + ) { + throw new Error('Expected workflowVersion to be present in the result'); + } + + expect(result.workflowVersion.id).toBe('v-draft'); + }); + + it('should return error when workflow is not found', async () => { + const context = buildContext(); + const deps = buildDeps({ workflow: null, versions: [] }); + + const tool = createGetWorkflowCurrentVersionTool( + deps as unknown as Pick< + WorkflowToolDependencies, + 'globalWorkspaceOrmManager' + >, + context, + ); + + const result = await tool.execute({ workflowId: WORKFLOW_ID }); + + expect(result.success).toBe(false); + expect(result.error).toContain(WORKFLOW_ID); + }); + + it('should return error when no draft or active version exists', async () => { + const context = buildContext(); + const deps = buildDeps({ workflow: { id: WORKFLOW_ID }, versions: [] }); + + const tool = createGetWorkflowCurrentVersionTool( + deps as unknown as Pick< + WorkflowToolDependencies, + 'globalWorkspaceOrmManager' + >, + context, + ); + + const result = await tool.execute({ workflowId: WORKFLOW_ID }); + + expect(result.success).toBe(false); + expect(result.error).toContain('no draft or active version'); + }); +}); diff --git a/packages/twenty-server/src/modules/workflow/workflow-tools/tools/__tests__/get-workflow-run.tool.spec.ts b/packages/twenty-server/src/modules/workflow/workflow-tools/tools/__tests__/get-workflow-run.tool.spec.ts new file mode 100644 index 0000000000..4269204127 --- /dev/null +++ b/packages/twenty-server/src/modules/workflow/workflow-tools/tools/__tests__/get-workflow-run.tool.spec.ts @@ -0,0 +1,120 @@ +import { StepStatus } from 'twenty-shared/workflow'; + +import { type WorkflowToolDependencies } from 'src/modules/workflow/workflow-tools/types/workflow-tool-dependencies.type'; + +import { createGetWorkflowRunTool } from '../get-workflow-run.tool'; + +const WORKSPACE_ID = '20202020-aaaa-4d02-bf25-6aeccf7ea419'; +const WORKFLOW_RUN_ID = '20202020-bbbb-4d02-bf25-6aeccf7ea419'; +const ROLE_ID = '20202020-cccc-4d02-bf25-6aeccf7ea419'; + +const buildDeps = (findOneResult: unknown) => ({ + globalWorkspaceOrmManager: { + executeInWorkspaceContext: jest.fn().mockImplementation(async (fn) => fn()), + getRepository: jest.fn().mockResolvedValue({ + findOne: jest.fn().mockResolvedValue(findOneResult), + }), + }, +}); + +const buildContext = () => ({ + workspaceId: WORKSPACE_ID, + rolePermissionConfig: { intersectionOf: [ROLE_ID] }, +}); + +describe('get_workflow_run tool', () => { + it('should pass rolePermissionConfig to getRepository', async () => { + const deps = buildDeps({ id: WORKFLOW_RUN_ID, state: {}, stepLogs: {} }); + const context = buildContext(); + + const tool = createGetWorkflowRunTool( + deps as unknown as Pick< + WorkflowToolDependencies, + 'globalWorkspaceOrmManager' + >, + context, + ); + + await tool.execute({ workflowRunId: WORKFLOW_RUN_ID }); + + expect(deps.globalWorkspaceOrmManager.getRepository).toHaveBeenCalledWith( + WORKSPACE_ID, + 'workflowRun', + context.rolePermissionConfig, + ); + }); + + it('should return workflow run details on success', async () => { + const workflowRun = { + id: WORKFLOW_RUN_ID, + name: 'Test Run', + status: 'FAILED', + startedAt: '2025-01-01T00:00:00Z', + endedAt: '2025-01-01T00:01:00Z', + enqueuedAt: null, + workflowId: 'wf-1', + workflowVersionId: 'wfv-1', + state: { + workflowRunError: 'Something went wrong', + flow: { + steps: [ + { id: 'step-1', name: 'Step 1', type: 'CODE' }, + { id: 'step-2', name: 'Step 2', type: 'SEND_EMAIL' }, + ], + }, + stepInfos: { + 'step-1': { status: StepStatus.SUCCESS }, + 'step-2': { status: StepStatus.FAILED, error: 'Email failed' }, + }, + }, + stepLogs: { + 'step-2': [{ message: 'SMTP error' }], + }, + }; + + const deps = buildDeps(workflowRun); + const context = buildContext(); + + const tool = createGetWorkflowRunTool( + deps as unknown as Pick< + WorkflowToolDependencies, + 'globalWorkspaceOrmManager' + >, + context, + ); + + const result = await tool.execute({ workflowRunId: WORKFLOW_RUN_ID }); + + expect(result.success).toBe(true); + + if (!('workflowRun' in result) || result.workflowRun === undefined) { + throw new Error('Expected workflowRun to be present in the result'); + } + + expect(result.workflowRun.id).toBe(WORKFLOW_RUN_ID); + expect(result.workflowRun.error).toBe('Something went wrong'); + expect(result.workflowRun.steps).toHaveLength(2); + expect(result.workflowRun.steps[1].error).toBe('Email failed'); + expect(result.workflowRun.failedStepLogs).toEqual({ + 'step-2': [{ message: 'SMTP error' }], + }); + }); + + it('should return error when workflow run is not found', async () => { + const deps = buildDeps(null); + const context = buildContext(); + + const tool = createGetWorkflowRunTool( + deps as unknown as Pick< + WorkflowToolDependencies, + 'globalWorkspaceOrmManager' + >, + context, + ); + + const result = await tool.execute({ workflowRunId: WORKFLOW_RUN_ID }); + + expect(result.success).toBe(false); + expect(result.error).toContain(WORKFLOW_RUN_ID); + }); +}); diff --git a/packages/twenty-server/src/modules/workflow/workflow-tools/tools/__tests__/list-workflow-runs.tool.spec.ts b/packages/twenty-server/src/modules/workflow/workflow-tools/tools/__tests__/list-workflow-runs.tool.spec.ts new file mode 100644 index 0000000000..9988c7219e --- /dev/null +++ b/packages/twenty-server/src/modules/workflow/workflow-tools/tools/__tests__/list-workflow-runs.tool.spec.ts @@ -0,0 +1,125 @@ +import { WorkflowRunStatus } from 'src/modules/workflow/common/standard-objects/workflow-run.workspace-entity'; +import { type WorkflowToolDependencies } from 'src/modules/workflow/workflow-tools/types/workflow-tool-dependencies.type'; + +import { createListWorkflowRunsTool } from '../list-workflow-runs.tool'; + +const WORKSPACE_ID = '20202020-aaaa-4d02-bf25-6aeccf7ea419'; +const ROLE_ID = '20202020-cccc-4d02-bf25-6aeccf7ea419'; + +const buildDeps = (findResult: unknown[]) => ({ + globalWorkspaceOrmManager: { + executeInWorkspaceContext: jest.fn().mockImplementation(async (fn) => fn()), + getRepository: jest.fn().mockResolvedValue({ + find: jest.fn().mockResolvedValue(findResult), + }), + }, +}); + +const buildContext = () => ({ + workspaceId: WORKSPACE_ID, + rolePermissionConfig: { intersectionOf: [ROLE_ID] }, +}); + +describe('list_workflow_runs tool', () => { + it('should pass rolePermissionConfig to getRepository', async () => { + const deps = buildDeps([]); + const context = buildContext(); + + const tool = createListWorkflowRunsTool( + deps as unknown as Pick< + WorkflowToolDependencies, + 'globalWorkspaceOrmManager' + >, + context, + ); + + await tool.execute({}); + + expect(deps.globalWorkspaceOrmManager.getRepository).toHaveBeenCalledWith( + WORKSPACE_ID, + 'workflowRun', + context.rolePermissionConfig, + ); + }); + + it('should return workflow runs on success', async () => { + const workflowRuns = [ + { + id: 'run-1', + name: 'Run 1', + status: WorkflowRunStatus.COMPLETED, + state: {}, + startedAt: '2025-01-01T00:00:00Z', + endedAt: '2025-01-01T00:01:00Z', + workflowId: 'wf-1', + workflowVersionId: 'wfv-1', + }, + { + id: 'run-2', + name: 'Run 2', + status: WorkflowRunStatus.FAILED, + state: { workflowRunError: 'Timeout' }, + startedAt: '2025-01-02T00:00:00Z', + endedAt: '2025-01-02T00:01:00Z', + workflowId: 'wf-1', + workflowVersionId: 'wfv-1', + }, + ]; + + const deps = buildDeps(workflowRuns); + const context = buildContext(); + + const tool = createListWorkflowRunsTool( + deps as unknown as Pick< + WorkflowToolDependencies, + 'globalWorkspaceOrmManager' + >, + context, + ); + + const result = await tool.execute({}); + + expect(result.success).toBe(true); + + if (!('workflowRuns' in result)) { + throw new Error('Expected workflowRuns to be present in the result'); + } + + expect(result.workflowRuns).toHaveLength(2); + expect(result.workflowRuns[0].id).toBe('run-1'); + expect(result.workflowRuns[1].error).toBe('Timeout'); + }); + + it('should apply filters when provided', async () => { + const findMock = jest.fn().mockResolvedValue([]); + const deps = { + globalWorkspaceOrmManager: { + executeInWorkspaceContext: jest + .fn() + .mockImplementation(async (fn) => fn()), + getRepository: jest.fn().mockResolvedValue({ find: findMock }), + }, + }; + const context = buildContext(); + + const tool = createListWorkflowRunsTool( + deps as unknown as Pick< + WorkflowToolDependencies, + 'globalWorkspaceOrmManager' + >, + context, + ); + + await tool.execute({ + workflowId: 'wf-1', + status: WorkflowRunStatus.FAILED, + limit: 5, + }); + + expect(findMock).toHaveBeenCalledWith({ + where: { workflowId: 'wf-1', status: WorkflowRunStatus.FAILED }, + order: { createdAt: 'DESC' }, + take: 5, + }); + }); +}); diff --git a/packages/twenty-server/src/modules/workflow/workflow-tools/tools/get-workflow-current-version.tool.ts b/packages/twenty-server/src/modules/workflow/workflow-tools/tools/get-workflow-current-version.tool.ts index c68ee12dc0..512f45ca30 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-tools/tools/get-workflow-current-version.tool.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-tools/tools/get-workflow-current-version.tool.ts @@ -1,6 +1,7 @@ import { isDefined } from 'twenty-shared/utils'; 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 { WorkflowVersionStatus, @@ -12,6 +13,10 @@ import { type WorkflowToolDependencies, } from 'src/modules/workflow/workflow-tools/types/workflow-tool-dependencies.type'; +type GetWorkflowCurrentVersionToolContext = WorkflowToolContext & { + rolePermissionConfig: RolePermissionConfig; +}; + const getWorkflowCurrentVersionSchema = z.object({ workflowId: z .string() @@ -25,7 +30,7 @@ type GetWorkflowCurrentVersionInput = z.infer< export const createGetWorkflowCurrentVersionTool = ( deps: Pick, - context: WorkflowToolContext, + context: GetWorkflowCurrentVersionToolContext, ) => ({ name: 'get_workflow_current_version' as const, description: @@ -41,7 +46,7 @@ export const createGetWorkflowCurrentVersionTool = ( await deps.globalWorkspaceOrmManager.getRepository( context.workspaceId, 'workflow', - { shouldBypassPermissionChecks: true }, + context.rolePermissionConfig, ); const workflow = await workflowRepository.findOne({ @@ -59,7 +64,7 @@ export const createGetWorkflowCurrentVersionTool = ( await deps.globalWorkspaceOrmManager.getRepository( context.workspaceId, 'workflowVersion', - { shouldBypassPermissionChecks: true }, + context.rolePermissionConfig, ); const versions = await workflowVersionRepository.find({ diff --git a/packages/twenty-server/src/modules/workflow/workflow-tools/tools/get-workflow-run.tool.ts b/packages/twenty-server/src/modules/workflow/workflow-tools/tools/get-workflow-run.tool.ts new file mode 100644 index 0000000000..cd07e733a7 --- /dev/null +++ b/packages/twenty-server/src/modules/workflow/workflow-tools/tools/get-workflow-run.tool.ts @@ -0,0 +1,113 @@ +import { isDefined } from 'twenty-shared/utils'; +import { StepStatus } from 'twenty-shared/workflow'; +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 WorkflowRunWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-run.workspace-entity'; +import { + type WorkflowToolContext, + type WorkflowToolDependencies, +} from 'src/modules/workflow/workflow-tools/types/workflow-tool-dependencies.type'; + +type GetWorkflowRunToolContext = WorkflowToolContext & { + rolePermissionConfig: RolePermissionConfig; +}; + +const getWorkflowRunSchema = z.object({ + workflowRunId: z.uuid().describe('The UUID of the workflow run to inspect'), +}); + +type GetWorkflowRunInput = z.infer; + +const FAILED_STEP_STATUSES: StepStatus[] = [ + StepStatus.FAILED, + StepStatus.FAILED_SAFELY, +]; + +export const createGetWorkflowRunTool = ( + deps: Pick, + context: GetWorkflowRunToolContext, +) => ({ + name: 'get_workflow_run' as const, + description: + 'Get the details of a single workflow run for troubleshooting. Returns the overall status, the run-level error, the status and error of each step, and the execution logs of the steps that failed. Use this to diagnose why a workflow run failed or behaved unexpectedly.', + inputSchema: getWorkflowRunSchema, + execute: async (parameters: GetWorkflowRunInput) => { + try { + const authContext = buildSystemAuthContext(context.workspaceId); + + return await deps.globalWorkspaceOrmManager.executeInWorkspaceContext( + async () => { + const workflowRunRepository = + await deps.globalWorkspaceOrmManager.getRepository( + context.workspaceId, + 'workflowRun', + context.rolePermissionConfig, + ); + + const workflowRun = await workflowRunRepository.findOne({ + where: { id: parameters.workflowRunId }, + }); + + if (!isDefined(workflowRun)) { + return { + success: false, + error: `Workflow run ${parameters.workflowRunId} not found`, + }; + } + + const stepInfos = workflowRun.state?.stepInfos ?? {}; + + const steps = (workflowRun.state?.flow?.steps ?? []).map((step) => { + const stepInfo = stepInfos[step.id]; + + return { + id: step.id, + name: step.name, + type: step.type, + status: stepInfo?.status, + error: stepInfo?.error, + }; + }); + + const failedStepIds = Object.entries(stepInfos) + .filter(([, stepInfo]) => + FAILED_STEP_STATUSES.includes(stepInfo.status), + ) + .map(([stepId]) => stepId); + + const failedStepLogs = Object.fromEntries( + Object.entries(workflowRun.stepLogs ?? {}).filter(([stepId]) => + failedStepIds.includes(stepId), + ), + ); + + return { + success: true, + workflowRun: { + id: workflowRun.id, + name: workflowRun.name, + status: workflowRun.status, + error: workflowRun.state?.workflowRunError, + startedAt: workflowRun.startedAt, + endedAt: workflowRun.endedAt, + enqueuedAt: workflowRun.enqueuedAt, + workflowId: workflowRun.workflowId, + workflowVersionId: workflowRun.workflowVersionId, + steps, + failedStepLogs, + }, + }; + }, + authContext, + ); + } catch (error) { + return { + success: false, + error: error.message, + message: `Failed to get workflow run: ${error.message}`, + }; + } + }, +}); diff --git a/packages/twenty-server/src/modules/workflow/workflow-tools/tools/list-workflow-runs.tool.ts b/packages/twenty-server/src/modules/workflow/workflow-tools/tools/list-workflow-runs.tool.ts new file mode 100644 index 0000000000..589b98a5de --- /dev/null +++ b/packages/twenty-server/src/modules/workflow/workflow-tools/tools/list-workflow-runs.tool.ts @@ -0,0 +1,106 @@ +import { isDefined } from 'twenty-shared/utils'; +import { type FindOptionsWhere } from 'typeorm'; +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 { + WorkflowRunStatus, + type WorkflowRunWorkspaceEntity, +} from 'src/modules/workflow/common/standard-objects/workflow-run.workspace-entity'; +import { + type WorkflowToolContext, + type WorkflowToolDependencies, +} from 'src/modules/workflow/workflow-tools/types/workflow-tool-dependencies.type'; + +type ListWorkflowRunsToolContext = WorkflowToolContext & { + rolePermissionConfig: RolePermissionConfig; +}; + +const DEFAULT_LIMIT = 10; +const MAX_LIMIT = 50; + +const listWorkflowRunsSchema = z.object({ + workflowId: z + .uuid() + .optional() + .describe('Filter runs by the UUID of the workflow they belong to'), + status: z + .nativeEnum(WorkflowRunStatus) + .optional() + .describe( + 'Filter runs by status (e.g. FAILED to find runs that need troubleshooting)', + ), + limit: z + .number() + .int() + .min(1) + .max(MAX_LIMIT) + .optional() + .describe(`Maximum number of runs to return (default ${DEFAULT_LIMIT})`), +}); + +type ListWorkflowRunsInput = z.infer; + +export const createListWorkflowRunsTool = ( + deps: Pick, + context: ListWorkflowRunsToolContext, +) => ({ + name: 'list_workflow_runs' as const, + description: + 'List workflow runs, optionally filtered by workflow and/or status, ordered from most to least recent. Use this to find the relevant run (for example the latest failed run of a workflow) before inspecting it in detail with get_workflow_run.', + inputSchema: listWorkflowRunsSchema, + execute: async (parameters: ListWorkflowRunsInput) => { + try { + const authContext = buildSystemAuthContext(context.workspaceId); + + return await deps.globalWorkspaceOrmManager.executeInWorkspaceContext( + async () => { + const workflowRunRepository = + await deps.globalWorkspaceOrmManager.getRepository( + context.workspaceId, + 'workflowRun', + context.rolePermissionConfig, + ); + + const where: FindOptionsWhere = {}; + + if (isDefined(parameters.workflowId)) { + where.workflowId = parameters.workflowId; + } + + if (isDefined(parameters.status)) { + where.status = parameters.status; + } + + const workflowRuns = await workflowRunRepository.find({ + where, + order: { createdAt: 'DESC' }, + take: parameters.limit ?? DEFAULT_LIMIT, + }); + + return { + success: true, + workflowRuns: workflowRuns.map((workflowRun) => ({ + id: workflowRun.id, + name: workflowRun.name, + status: workflowRun.status, + error: workflowRun.state?.workflowRunError, + startedAt: workflowRun.startedAt, + endedAt: workflowRun.endedAt, + workflowId: workflowRun.workflowId, + workflowVersionId: workflowRun.workflowVersionId, + })), + }; + }, + authContext, + ); + } catch (error) { + return { + success: false, + error: error.message, + message: `Failed to list workflow runs: ${error.message}`, + }; + } + }, +});