fix: exclude system objects and workflow/dashboard from AI/MCP write tool descriptors (#20973)

## Summary

fix: exclude system join objects from AI/MCP create/update/delete tool
descriptors

Closes #20403

---
AI was used for assistance.

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Félix Malfait <felix@twenty.com>
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
This commit is contained in:
Matt Van Horn
2026-05-27 11:01:11 -07:00
committed by GitHub
parent c0cbe67bcd
commit de7daaa81a
18 changed files with 283 additions and 115 deletions
@@ -0,0 +1,19 @@
// Objects whose records must not be created, updated, or deleted by
// automation callers (workflows and AI tools). Either they back the
// automation runtime itself (recursion risk), gate access/permissions,
// or are owned by background sync (writing to them corrupts state).
export const OBJECTS_BLOCKED_FROM_AUTOMATION = [
'workflow',
'workflowVersion',
'workflowRun',
'workflowAutomatedTrigger',
'workspaceMember',
'dashboard',
'message',
'messageThread',
'messageChannelMessageAssociation',
'messageParticipant',
'calendarEvent',
'calendarEventParticipant',
'calendarChannelEventAssociation',
] as const;
+2 -1
View File
@@ -10,6 +10,7 @@
export { CAPTURE_ALL_VARIABLE_TAG_INNER_REGEX } from './constants/CaptureAllVariableTagInnerRegex';
export { CONTENT_TYPE_VALUES_HTTP_REQUEST } from './constants/ContentTypeValuesHttpRequest';
export { IF_ELSE_BRANCH_POSITION_OFFSETS } from './constants/IfElseBranchPositionOffsets';
export { OBJECTS_BLOCKED_FROM_AUTOMATION } from './constants/ObjectsBlockedFromAutomation';
export { TRIGGER_STEP_ID } from './constants/TriggerStepId';
export { workflowAiAgentActionSchema } from './schemas/ai-agent-action-schema';
export { workflowAiAgentActionSettingsSchema } from './schemas/ai-agent-action-settings-schema';
@@ -81,7 +82,7 @@ export type {
WorkflowRunStepInfos,
} from './types/WorkflowRunStateStepInfos';
export { StepStatus } from './types/WorkflowRunStateStepInfos';
export { canObjectBeManagedByWorkflow } from './utils/canObjectBeManagedByWorkflow';
export { canObjectBeManagedByAutomation } from './utils/canObjectBeManagedByAutomation';
export { extractRawVariableNamePart } from './utils/extractRawVariableNameParts';
export { getFunctionInputFromInputSchema } from './utils/getFunctionInputFromInputSchema';
export { getWorkflowRunContext } from './utils/getWorkflowRunContext';
@@ -0,0 +1,45 @@
import { canObjectBeManagedByAutomation } from '@/workflow/utils/canObjectBeManagedByAutomation';
describe('canObjectBeManagedByAutomation', () => {
it('should return true for a standard non-blocked object', () => {
expect(canObjectBeManagedByAutomation({ nameSingular: 'company' })).toBe(
true,
);
});
it('should return true for noteTarget and taskTarget', () => {
expect(canObjectBeManagedByAutomation({ nameSingular: 'noteTarget' })).toBe(
true,
);
expect(canObjectBeManagedByAutomation({ nameSingular: 'taskTarget' })).toBe(
true,
);
});
it('should return true for attachment and timelineActivity', () => {
expect(canObjectBeManagedByAutomation({ nameSingular: 'attachment' })).toBe(
true,
);
expect(
canObjectBeManagedByAutomation({ nameSingular: 'timelineActivity' }),
).toBe(true);
});
it.each([
'workflow',
'workflowVersion',
'workflowRun',
'workflowAutomatedTrigger',
'workspaceMember',
'dashboard',
'message',
'messageThread',
'messageChannelMessageAssociation',
'messageParticipant',
'calendarEvent',
'calendarEventParticipant',
'calendarChannelEventAssociation',
])('should return false for %s', (nameSingular) => {
expect(canObjectBeManagedByAutomation({ nameSingular })).toBe(false);
});
});
@@ -1,57 +0,0 @@
import { canObjectBeManagedByWorkflow } from '@/workflow/utils/canObjectBeManagedByWorkflow';
describe('canObjectBeManagedByWorkflow', () => {
it('should return true for non-system, non-excluded objects', () => {
expect(
canObjectBeManagedByWorkflow({
nameSingular: 'company',
isSystem: false,
}),
).toBe(true);
});
it('should return false for system objects', () => {
expect(
canObjectBeManagedByWorkflow({
nameSingular: 'company',
isSystem: true,
}),
).toBe(false);
});
it('should return false for workflow object', () => {
expect(
canObjectBeManagedByWorkflow({
nameSingular: 'workflow',
isSystem: false,
}),
).toBe(false);
});
it('should return false for workflowVersion object', () => {
expect(
canObjectBeManagedByWorkflow({
nameSingular: 'workflowVersion',
isSystem: false,
}),
).toBe(false);
});
it('should return false for workflowRun object', () => {
expect(
canObjectBeManagedByWorkflow({
nameSingular: 'workflowRun',
isSystem: false,
}),
).toBe(false);
});
it('should return false for dashboard object', () => {
expect(
canObjectBeManagedByWorkflow({
nameSingular: 'dashboard',
isSystem: false,
}),
).toBe(false);
});
});
@@ -0,0 +1,11 @@
import { OBJECTS_BLOCKED_FROM_AUTOMATION } from '../constants/ObjectsBlockedFromAutomation';
export const canObjectBeManagedByAutomation = ({
nameSingular,
}: {
nameSingular: string;
}): boolean => {
return !OBJECTS_BLOCKED_FROM_AUTOMATION.includes(
nameSingular as (typeof OBJECTS_BLOCKED_FROM_AUTOMATION)[number],
);
};
@@ -1,19 +0,0 @@
export const canObjectBeManagedByWorkflow = ({
nameSingular,
isSystem,
}: {
nameSingular: string;
isSystem: boolean;
}) => {
const excludedNonSystemObjectMetadataItemNames = [
'workflow',
'workflowVersion',
'workflowRun',
'dashboard',
];
return (
!excludedNonSystemObjectMetadataItemNames.includes(nameSingular) &&
!isSystem
);
};