feat(workflow) - Add validation layer (#21422)

Add workflow validation framework and consolidate output schema
types/search logic into twenty-shared

This PR introduces a comprehensive workflow validation system that
catches configuration errors at build-time, and consolidates the
fragmented output-schema type definitions and variable-search logic from
the front-end into twenty-shared

**Workflow validation** — A new system that checks workflows for errors
before activation: graph connectivity (unreachable steps, dangling
references), step parameter schemas (via Zod), variable references
(typos, wrong step order), and workspace metadata (non-existent
objects). Returns structured errors/warnings with "did you mean?"
suggestions. Runs automatically after create_complete_workflow and
update_workflow_version_step, and is also available as a standalone
validate_workflow tool.

**Output schema consolidation** — Moves all output schema types and the
variable-search logic from scattered front-end files into twenty-shared,
replacing ~800 lines of duplicated per-schema-type code with a single
unified searchVariableInOutputSchema dispatcher.


To do : 
- validation on CODE and AGENT step

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Etienne
2026-06-12 10:23:03 +02:00
committed by GitHub
parent 2538239e05
commit fefd9d7704
120 changed files with 4445 additions and 1192 deletions
@@ -1,28 +1,30 @@
import { checkUrlType } from '~/utils/checkUrlType';
import { LinkType } from 'twenty-ui-deprecated/navigation';
describe('checkUrlType', () => {
it('should return "linkedin", if linkedin url', () => {
expect(checkUrlType('https://www.linkedin.com/in/håkan-fisk')).toBe(
'linkedin',
it('should detect LinkedIn urls', () => {
expect(checkUrlType('https://www.linkedin.com/in/john')).toBe(
LinkType.LinkedIn,
);
expect(checkUrlType('http://www.linkedin.com/in/håkan-fisk')).toBe(
'linkedin',
expect(checkUrlType('linkedin.com/company/twenty')).toBe(LinkType.LinkedIn);
});
it('should detect Twitter urls', () => {
expect(checkUrlType('https://twitter.com/twenty')).toBe(LinkType.Twitter);
});
it('should detect X urls as Twitter', () => {
expect(checkUrlType('https://x.com/twenty')).toBe(LinkType.Twitter);
});
it('should detect Facebook urls', () => {
expect(checkUrlType('https://www.facebook.com/twenty')).toBe(
LinkType.Facebook,
);
expect(checkUrlType('https://linkedin.com/in/håkan-fisk')).toBe('linkedin');
expect(checkUrlType('http://linkedin.com/in/håkan-fisk')).toBe('linkedin');
expect(checkUrlType('linkedin.com/in/håkan-fisk')).toBe('linkedin');
});
it('should return "twitter", if twitter url', () => {
expect(checkUrlType('https://www.twitter.com/john-doe')).toBe('twitter');
expect(checkUrlType('https://www.x.com/john-doe')).toBe('twitter');
});
it('should return "url", if neither linkedin nor twitter url', () => {
expect(checkUrlType('https://www.example.com')).toBe('url');
});
it('should return "facebook", if facebook url', () => {
expect(checkUrlType('https://www.facebook.com/john-doe')).toBe('facebook');
it('should fall back to a generic url type', () => {
expect(checkUrlType('https://example.com')).toBe(LinkType.Url);
expect(checkUrlType('not-a-url')).toBe(LinkType.Url);
});
});
@@ -0,0 +1,19 @@
import { compareNonEmptyStrings } from '~/utils/compareNonEmptyStrings';
describe('compareNonEmptyStrings', () => {
it('should return true when both values are empty or nullish', () => {
expect(compareNonEmptyStrings(null, undefined)).toBe(true);
expect(compareNonEmptyStrings('', null)).toBe(true);
expect(compareNonEmptyStrings('', '')).toBe(true);
});
it('should return true when both values are equal non-empty strings', () => {
expect(compareNonEmptyStrings('foo', 'foo')).toBe(true);
});
it('should return false when values differ', () => {
expect(compareNonEmptyStrings('foo', 'bar')).toBe(false);
expect(compareNonEmptyStrings('foo', '')).toBe(false);
expect(compareNonEmptyStrings('foo', null)).toBe(false);
});
});
@@ -1,49 +1,24 @@
import { compareStrictlyExceptForNullAndUndefined } from '~/utils/compareStrictlyExceptForNullAndUndefined';
describe('compareStrictlyExceptForNullAndUndefined', () => {
it('should return true for undefined === null', () => {
it('should return true when both values are nullish', () => {
expect(compareStrictlyExceptForNullAndUndefined(null, undefined)).toBe(
true,
);
expect(compareStrictlyExceptForNullAndUndefined(undefined, null)).toBe(
true,
);
});
it('should return true for null === undefined', () => {
expect(compareStrictlyExceptForNullAndUndefined(null, undefined)).toBe(
true,
);
it('should compare strictly when at least one value is defined', () => {
expect(compareStrictlyExceptForNullAndUndefined(1, 1)).toBe(true);
expect(compareStrictlyExceptForNullAndUndefined('a', 'a')).toBe(true);
expect(compareStrictlyExceptForNullAndUndefined(1, 2)).toBe(false);
expect(compareStrictlyExceptForNullAndUndefined(1, null)).toBe(false);
});
it('should return true for undefined === undefined', () => {
expect(compareStrictlyExceptForNullAndUndefined(undefined, undefined)).toBe(
true,
);
});
it('should return true for null === null', () => {
expect(compareStrictlyExceptForNullAndUndefined(null, null)).toBe(true);
});
it('should return true for 2 === 2', () => {
expect(compareStrictlyExceptForNullAndUndefined(2, 2)).toBe(true);
});
it('should return false for 2 === 3', () => {
expect(compareStrictlyExceptForNullAndUndefined(2, 3)).toBe(false);
});
it('should return false for undefined === 2', () => {
expect(compareStrictlyExceptForNullAndUndefined(undefined, 2)).toBe(false);
});
it('should return false for null === 2', () => {
expect(compareStrictlyExceptForNullAndUndefined(null, 2)).toBe(false);
});
it('should return false for 2 === "2"', () => {
expect(compareStrictlyExceptForNullAndUndefined(2, '2')).toBe(false);
});
it('should return true for "2" === "2"', () => {
expect(compareStrictlyExceptForNullAndUndefined('2', '2')).toBe(true);
it('should not treat zero or empty string as nullish', () => {
expect(compareStrictlyExceptForNullAndUndefined(0, null)).toBe(false);
expect(compareStrictlyExceptForNullAndUndefined('', undefined)).toBe(false);
});
});