fix(ai) - workflow tool outputs optim + display fix (#21500)

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21500?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:
Etienne
2026-06-16 10:50:16 +02:00
committed by GitHub
parent 12b1dba986
commit ceb7698689
7 changed files with 286 additions and 10 deletions
@@ -0,0 +1,105 @@
import { createUpdateWorkflowVersionStepTool } from 'src/modules/workflow/workflow-tools/tools/update-workflow-version-step.tool';
const mockStep = {
id: 'f47ac10b-58cc-4372-a567-0e02b2c3d479',
name: 'Send email',
type: 'SEND_EMAIL',
valid: true,
settings: { input: {} },
};
const buildTool = ({
validationResult = { valid: true, errors: [], warnings: [] },
}: {
validationResult?: object;
} = {}) => {
const workflowVersionStepService = {
updateWorkflowVersionStep: jest.fn().mockResolvedValue(mockStep),
};
const workflowValidationService = {
validateWorkflowVersion: jest.fn().mockResolvedValue(validationResult),
};
const tool = createUpdateWorkflowVersionStepTool(
{
workflowVersionStepService,
workflowValidationService,
} as never,
{ workspaceId: 'workspace-id' },
);
return { tool, workflowVersionStepService, workflowValidationService };
};
const baseInput = {
workflowVersionId: 'b3b8a4f0-0000-4000-8000-000000000000',
step: mockStep,
} as unknown as Parameters<
ReturnType<typeof createUpdateWorkflowVersionStepTool>['execute']
>[0];
describe('createUpdateWorkflowVersionStepTool', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should validate by default and return a compact summary', async () => {
const { tool, workflowValidationService } = buildTool({
validationResult: {
valid: false,
errors: [
{
severity: 'error',
code: 'DANGLING_REFERENCE',
message: 'Unknown variable',
availablePaths: ['{{trigger.x}}'],
},
],
warnings: [{ severity: 'warning', code: 'NO_STEPS', message: 'w' }],
},
});
const result = (await tool.execute(baseInput)) as Record<string, unknown>;
expect(
workflowValidationService.validateWorkflowVersion,
).toHaveBeenCalled();
const validation = result.validation as Record<string, unknown>;
expect(validation.valid).toBe(false);
expect(validation.errorCount).toBe(1);
expect(validation.warningCount).toBe(1);
expect(validation).not.toHaveProperty('warnings');
expect((validation.errors as object[])[0]).not.toHaveProperty(
'availablePaths',
);
});
it('should skip validation entirely when validate is false', async () => {
const { tool, workflowValidationService } = buildTool();
const result = (await tool.execute({
...baseInput,
validate: false,
})) as Record<string, unknown>;
expect(
workflowValidationService.validateWorkflowVersion,
).not.toHaveBeenCalled();
expect(result).not.toHaveProperty('validation');
});
it('should still return the step result when validation throws', async () => {
const { tool, workflowValidationService } = buildTool();
workflowValidationService.validateWorkflowVersion.mockRejectedValue(
new Error('boom'),
);
const result = (await tool.execute(baseInput)) as Record<string, unknown>;
expect(result.validationError).toBe('boom');
expect(result).not.toHaveProperty('validation');
});
});
@@ -18,6 +18,7 @@ import {
type WorkflowToolContext,
type WorkflowToolDependencies,
} from 'src/modules/workflow/workflow-tools/types/workflow-tool-dependencies.type';
import { summarizeValidation } from 'src/modules/workflow/workflow-tools/utils/summarize-validation.util';
import { type WorkflowTrigger } from 'src/modules/workflow/workflow-trigger/types/workflow-trigger.type';
const createCompleteWorkflowSchema = z.object({
@@ -106,7 +107,9 @@ IMPORTANT: The tool schema provides comprehensive field descriptions, examples,
- Variable reference syntax: {{trigger.fieldName}} for trigger data, {{<step-id>.result.fieldName}} for step outputs (step-id is the step's UUID, not its name)
- Error handling options
This is the most efficient way for AI to create workflows as it handles all the complexity in one call.`,
This is the most efficient way for AI to create workflows as it handles all the complexity in one call.
The response includes a compact validation summary. For the full validation report with available variable paths, call validate_workflow once after your edits — not after every change.`,
inputSchema: createCompleteWorkflowSchema,
execute: async (parameters: {
name: string;
@@ -197,9 +200,8 @@ This is the most efficient way for AI to create workflows as it handles all the
workflowId,
workflowVersionId,
name: parameters.name,
trigger: parameters.trigger,
steps: parameters.steps,
validation,
stepIds: parameters.steps.map((step) => step.id),
validation: summarizeValidation(validation),
},
recordReferences: [
{
@@ -6,6 +6,7 @@ import {
type WorkflowToolContext,
type WorkflowToolDependencies,
} from 'src/modules/workflow/workflow-tools/types/workflow-tool-dependencies.type';
import { summarizeValidation } from 'src/modules/workflow/workflow-tools/utils/summarize-validation.util';
const updateWorkflowVersionStepSchema = z.object({
workflowVersionId: z
@@ -15,8 +16,19 @@ const updateWorkflowVersionStepSchema = z.object({
step: z
.union([workflowActionSchema])
.describe('The updated step configuration'),
validate: z
.boolean()
.optional()
.default(true)
.describe(
'Run a quick validation and return a compact summary (default true). Set to false when making several edits in a row, then call validate_workflow once at the end instead.',
),
});
type UpdateWorkflowVersionStepToolInput = UpdateWorkflowVersionStepInput & {
validate?: boolean;
};
export const createUpdateWorkflowVersionStepTool = (
deps: Pick<
WorkflowToolDependencies,
@@ -26,9 +38,9 @@ export const createUpdateWorkflowVersionStepTool = (
) => ({
name: 'update_workflow_version_step' as const,
description:
'Update an existing step in a workflow version. This modifies the step configuration.',
'Update an existing step in a workflow version. This modifies the step configuration. Returns a compact validation summary; for the full report with available variable paths, call validate_workflow once after your edits — not after every change.',
inputSchema: updateWorkflowVersionStepSchema,
execute: async (parameters: UpdateWorkflowVersionStepInput) => {
execute: async (parameters: UpdateWorkflowVersionStepToolInput) => {
let result;
try {
@@ -45,15 +57,20 @@ export const createUpdateWorkflowVersionStepTool = (
};
}
if (parameters.validate === false) {
return result;
}
try {
const validation =
await deps.workflowValidationService.validateWorkflowVersion({
workspaceId: context.workspaceId,
workflowVersionId: parameters.workflowVersionId,
});
return {
...result,
validation,
validation: summarizeValidation(validation),
};
} catch (error) {
return {
@@ -0,0 +1,97 @@
import { type WorkflowValidationResult } from 'twenty-shared/workflow';
import { summarizeValidation } from 'src/modules/workflow/workflow-tools/utils/summarize-validation.util';
describe('summarizeValidation', () => {
it('should return a minimal summary for a valid workflow', () => {
const result: WorkflowValidationResult = {
valid: true,
errors: [],
warnings: [],
};
expect(summarizeValidation(result)).toEqual({
valid: true,
errorCount: 0,
warningCount: 0,
errors: [],
});
});
it('should keep error essentials and drop availablePaths and hint', () => {
const result: WorkflowValidationResult = {
valid: false,
errors: [
{
severity: 'error',
code: 'DANGLING_REFERENCE',
message: 'Unknown variable {{step-1.foo}}',
stepId: 'step-2',
path: 'settings.input.body',
hint: 'some long hint',
suggestions: ['{{step-1.bar}}'],
availablePaths: ['{{step-1.bar}}', '{{step-1.baz}}', '{{trigger.x}}'],
},
],
warnings: [],
};
const summary = summarizeValidation(result);
expect(summary.valid).toBe(false);
expect(summary.errorCount).toBe(1);
expect(summary.errors[0]).toEqual({
code: 'DANGLING_REFERENCE',
message: 'Unknown variable {{step-1.foo}}',
stepId: 'step-2',
path: 'settings.input.body',
suggestions: ['{{step-1.bar}}'],
});
expect(summary.errors[0]).not.toHaveProperty('availablePaths');
expect(summary.errors[0]).not.toHaveProperty('hint');
});
it('should collapse warnings to a count', () => {
const result: WorkflowValidationResult = {
valid: true,
errors: [],
warnings: [
{
severity: 'warning',
code: 'UNREACHABLE_STEP',
message: 'Step is unreachable',
},
{
severity: 'warning',
code: 'TRIGGER_HAS_NO_NEXT_STEP',
message: 'Trigger has no next step',
},
],
};
const summary = summarizeValidation(result);
expect(summary.warningCount).toBe(2);
expect(summary).not.toHaveProperty('warnings');
expect(summary.hint).toContain('validate_workflow');
});
it('should omit empty suggestions', () => {
const result: WorkflowValidationResult = {
valid: false,
errors: [
{
severity: 'error',
code: 'NO_STEPS',
message: 'The workflow has no steps.',
suggestions: [],
},
],
warnings: [],
};
expect(summarizeValidation(result).errors[0]).not.toHaveProperty(
'suggestions',
);
});
});
@@ -0,0 +1,43 @@
import {
type WorkflowValidationIssue,
type WorkflowValidationResult,
} from 'twenty-shared/workflow';
export type WorkflowValidationSummary = {
valid: boolean;
errorCount: number;
warningCount: number;
errors: Array<
Pick<
WorkflowValidationIssue,
'code' | 'stepId' | 'path' | 'message' | 'suggestions'
>
>;
hint?: string;
};
export const summarizeValidation = (
result: WorkflowValidationResult,
): WorkflowValidationSummary => {
const summary: WorkflowValidationSummary = {
valid: result.valid,
errorCount: result.errors.length,
warningCount: result.warnings.length,
errors: result.errors.map(
({ code, stepId, path, message, suggestions }) => ({
code,
stepId,
path,
message,
...(suggestions && suggestions.length > 0 ? { suggestions } : {}),
}),
),
};
if (!result.valid || result.warnings.length > 0) {
summary.hint =
'Compact summary. For the full report including warnings and available variable paths, call validate_workflow.';
}
return summary;
};