feat(ai): add AI tools to list and inspect workflow runs (#21983)

- Add `get_workflow_run` and `list_workflow_runs` AI tools so the
workflow agent can troubleshoot failed or misbehaving workflow runs —
listing runs with optional filters (workflow, status, limit) and
inspecting a specific run's steps, errors, and failed step logs.
- Enforce `rolePermissionConfig` on all three read tools
(`get_workflow_run`, `list_workflow_runs`,
`get_workflow_current_version`) instead of bypassing permission checks,
consistent with how `create_complete_workflow` and database CRUD tools
work.
- Add unit tests for the three tools covering permission forwarding,
success paths, and error paths.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21983?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. -->

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Etienne
2026-06-23 11:19:42 +02:00
committed by GitHub
parent 47b48d83f7
commit 4789ba6265
8 changed files with 669 additions and 5 deletions
@@ -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": "<version-id>", "defaultSettings": { "input": { "logicFunctionId": "<logic-function-id>" } } }
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.
@@ -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,
@@ -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');
});
});
@@ -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);
});
});
@@ -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,
});
});
});
@@ -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<WorkflowToolDependencies, 'globalWorkspaceOrmManager'>,
context: WorkflowToolContext,
context: GetWorkflowCurrentVersionToolContext,
) => ({
name: 'get_workflow_current_version' as const,
description:
@@ -41,7 +46,7 @@ export const createGetWorkflowCurrentVersionTool = (
await deps.globalWorkspaceOrmManager.getRepository<WorkflowWorkspaceEntity>(
context.workspaceId,
'workflow',
{ shouldBypassPermissionChecks: true },
context.rolePermissionConfig,
);
const workflow = await workflowRepository.findOne({
@@ -59,7 +64,7 @@ export const createGetWorkflowCurrentVersionTool = (
await deps.globalWorkspaceOrmManager.getRepository<WorkflowVersionWorkspaceEntity>(
context.workspaceId,
'workflowVersion',
{ shouldBypassPermissionChecks: true },
context.rolePermissionConfig,
);
const versions = await workflowVersionRepository.find({
@@ -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<typeof getWorkflowRunSchema>;
const FAILED_STEP_STATUSES: StepStatus[] = [
StepStatus.FAILED,
StepStatus.FAILED_SAFELY,
];
export const createGetWorkflowRunTool = (
deps: Pick<WorkflowToolDependencies, 'globalWorkspaceOrmManager'>,
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<WorkflowRunWorkspaceEntity>(
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}`,
};
}
},
});
@@ -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<typeof listWorkflowRunsSchema>;
export const createListWorkflowRunsTool = (
deps: Pick<WorkflowToolDependencies, 'globalWorkspaceOrmManager'>,
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<WorkflowRunWorkspaceEntity>(
context.workspaceId,
'workflowRun',
context.rolePermissionConfig,
);
const where: FindOptionsWhere<WorkflowRunWorkspaceEntity> = {};
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}`,
};
}
},
});