From 4789ba62659e0993322052d9ae3146e71a57a5a5 Mon Sep 17 00:00:00 2001
From: Etienne <45695613+etiennejouan@users.noreply.github.com>
Date: Tue, 23 Jun 2026 11:19:42 +0200
Subject: [PATCH] feat(ai): add AI tools to list and inspect workflow runs
(#21983)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 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.
---------
Co-authored-by: Charles Bochet
---
...reate-standard-flat-skill-metadata.util.ts | 18 ++
.../workflow-tool.workspace-service.ts | 16 +-
.../get-workflow-current-version.tool.spec.ts | 165 ++++++++++++++++++
.../__tests__/get-workflow-run.tool.spec.ts | 120 +++++++++++++
.../__tests__/list-workflow-runs.tool.spec.ts | 125 +++++++++++++
.../get-workflow-current-version.tool.ts | 11 +-
.../tools/get-workflow-run.tool.ts | 113 ++++++++++++
.../tools/list-workflow-runs.tool.ts | 106 +++++++++++
8 files changed, 669 insertions(+), 5 deletions(-)
create mode 100644 packages/twenty-server/src/modules/workflow/workflow-tools/tools/__tests__/get-workflow-current-version.tool.spec.ts
create mode 100644 packages/twenty-server/src/modules/workflow/workflow-tools/tools/__tests__/get-workflow-run.tool.spec.ts
create mode 100644 packages/twenty-server/src/modules/workflow/workflow-tools/tools/__tests__/list-workflow-runs.tool.spec.ts
create mode 100644 packages/twenty-server/src/modules/workflow/workflow-tools/tools/get-workflow-run.tool.ts
create mode 100644 packages/twenty-server/src/modules/workflow/workflow-tools/tools/list-workflow-runs.tool.ts
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}`,
+ };
+ }
+ },
+});