feat(apps): split AI tool and workflow action triggers in LogicFunction manifest (#20208)
## Summary Replaces the bolted-on `isTool` + `toolInputSchema` fields on `LogicFunctionManifest` with two distinct, opt-in triggers that align with the existing `cron` / `databaseEvent` / `httpRoute` trigger pattern: - **`toolTriggerSettings`** — exposes the function as an AI tool (chat / MCP / function calling). Uses standard JSON Schema (the format LLMs natively understand). - **`workflowActionTriggerSettings`** — exposes the function as a step in the visual workflow builder. Uses Twenty's rich `InputSchema` so the builder can render proper `FieldMetadataType`-aware editors, variable pickers, labels, and an optional `outputSchema`. A function can opt into none, one, or both. Each surface gets the schema format appropriate for it. ### Why `isTool: true` previously exposed the function as both an AI tool AND a workflow node, with the same JSON Schema feeding both — but the workflow builder really wants Twenty's `InputSchema` (with `CURRENCY`, `RELATION`, `EMAILS`, etc.) and the AI surface really wants standard JSON Schema. Today the workflow builder hacks around this by treating JSON Schema as `InputSchema`, which silently breaks for any non-primitive field type. Splitting the triggers fixes that and lets each surface evolve independently. ### Migration - **Fast** instance command adds the two new nullable columns. - **Slow** instance command backfills `toolTriggerSettings` + `workflowActionTriggerSettings` from `isTool=true` rows (preserving today's both-surfaces behaviour) then drops the legacy columns. ### Stacked Stacked on top of #20181. Merge that first, then this. ## Test plan - [ ] CI green (oxlint, typecheck, jest, vitest) - [ ] Run `--include-slow` upgrade against a workspace with existing `isTool=true` logic functions; verify both new columns populated and old columns dropped - [ ] Verify AI chat sees migrated tool functions (Linear create-issue, Exa search) and can call them with the JSON Schema - [ ] Add an AI-tool function from the Settings UI (toggles `toolTriggerSettings`) and verify it shows up in chat - [ ] Add a workflow-action function from the Settings UI (toggles `workflowActionTriggerSettings`) and verify it appears in the workflow node picker - [ ] In the workflow builder, edit a `LOGIC_FUNCTION` step and verify input fields render (no more JSON-Schema-as-InputSchema hack) - [ ] Try defining a function with no triggers in the SDK and verify `defineLogicFunction` rejects it 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: martmull <martmull@hotmail.fr>
This commit is contained in:
+3
-4
@@ -75,8 +75,7 @@ export const SettingsLogicFunctionTestTab = ({
|
||||
httpRouteTriggerSettings,
|
||||
cronTriggerSettings,
|
||||
databaseEventTriggerSettings,
|
||||
toolInputSchema,
|
||||
isTool,
|
||||
toolTriggerSettings,
|
||||
} = formValues;
|
||||
|
||||
const triggerButtons: TriggerButton[] = [];
|
||||
@@ -93,7 +92,7 @@ export const SettingsLogicFunctionTestTab = ({
|
||||
Icon: IconDatabase,
|
||||
});
|
||||
}
|
||||
if (isTool) {
|
||||
if (isDefined(toolTriggerSettings)) {
|
||||
triggerButtons.push({ kind: 'tool', label: t`AI tool`, Icon: IconTool });
|
||||
}
|
||||
|
||||
@@ -119,7 +118,7 @@ export const SettingsLogicFunctionTestTab = ({
|
||||
? buildDatabaseEventPayload(databaseEventTriggerSettings)
|
||||
: {};
|
||||
case 'tool':
|
||||
return buildToolPayloadFromSchema(toolInputSchema);
|
||||
return buildToolPayloadFromSchema(toolTriggerSettings?.inputSchema);
|
||||
}
|
||||
})();
|
||||
updateLogicFunctionInput(payload);
|
||||
|
||||
+10
-4
@@ -3,6 +3,7 @@ import { SettingsLogicFunctionCronTriggerSection } from '@/settings/logic-functi
|
||||
import { SettingsLogicFunctionDatabaseEventTriggerSection } from '@/settings/logic-functions/components/triggers/SettingsLogicFunctionDatabaseEventTriggerSection';
|
||||
import { SettingsLogicFunctionHttpTriggerSection } from '@/settings/logic-functions/components/triggers/SettingsLogicFunctionHttpTriggerSection';
|
||||
import { SettingsLogicFunctionToolTriggerSection } from '@/settings/logic-functions/components/triggers/SettingsLogicFunctionToolTriggerSection';
|
||||
import { SettingsLogicFunctionWorkflowActionTriggerSection } from '@/settings/logic-functions/components/triggers/SettingsLogicFunctionWorkflowActionTriggerSection';
|
||||
import { styled } from '@linaria/react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
@@ -42,7 +43,8 @@ export const SettingsLogicFunctionTriggersTab = ({
|
||||
isDefined(formValues.httpRouteTriggerSettings) ||
|
||||
isDefined(formValues.cronTriggerSettings) ||
|
||||
isDefined(formValues.databaseEventTriggerSettings) ||
|
||||
formValues.isTool;
|
||||
isDefined(formValues.toolTriggerSettings) ||
|
||||
isDefined(formValues.workflowActionTriggerSettings);
|
||||
|
||||
if (readonly && !hasAnyTrigger) {
|
||||
return isDefined(applicationName) ? (
|
||||
@@ -79,9 +81,13 @@ export const SettingsLogicFunctionTriggersTab = ({
|
||||
readonly={readonly}
|
||||
/>
|
||||
<SettingsLogicFunctionToolTriggerSection
|
||||
isTool={formValues.isTool}
|
||||
toolInputSchema={formValues.toolInputSchema}
|
||||
onChange={onChange('isTool')}
|
||||
value={formValues.toolTriggerSettings}
|
||||
onChange={onChange('toolTriggerSettings')}
|
||||
readonly={readonly}
|
||||
/>
|
||||
<SettingsLogicFunctionWorkflowActionTriggerSection
|
||||
value={formValues.workflowActionTriggerSettings}
|
||||
onChange={onChange('workflowActionTriggerSettings')}
|
||||
readonly={readonly}
|
||||
/>
|
||||
{!readonly && !hasAnyTrigger && (
|
||||
|
||||
+15
-9
@@ -1,29 +1,33 @@
|
||||
import { SettingsLogicFunctionTriggerSection } from '@/settings/logic-functions/components/triggers/SettingsLogicFunctionTriggerSection';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { type ToolTriggerSettings } from 'twenty-shared/application';
|
||||
import { DEFAULT_TOOL_INPUT_SCHEMA } from 'twenty-shared/logic-function';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { SettingsToolParameterTable } from '~/pages/settings/ai/components/SettingsToolParameterTable';
|
||||
|
||||
const DEFAULT_TOOL_TRIGGER_SETTINGS: ToolTriggerSettings = {
|
||||
inputSchema: DEFAULT_TOOL_INPUT_SCHEMA,
|
||||
};
|
||||
|
||||
type ToolInputSchema = {
|
||||
properties?: Record<string, unknown>;
|
||||
required?: string[];
|
||||
};
|
||||
|
||||
type SettingsLogicFunctionToolTriggerSectionProps = {
|
||||
isTool: boolean;
|
||||
toolInputSchema?: object;
|
||||
onChange: (value: boolean) => void;
|
||||
value: ToolTriggerSettings | null;
|
||||
onChange: (value: ToolTriggerSettings | null) => void;
|
||||
readonly: boolean;
|
||||
};
|
||||
|
||||
export const SettingsLogicFunctionToolTriggerSection = ({
|
||||
isTool,
|
||||
toolInputSchema,
|
||||
value,
|
||||
onChange,
|
||||
readonly,
|
||||
}: SettingsLogicFunctionToolTriggerSectionProps) => {
|
||||
const { t } = useLingui();
|
||||
|
||||
const schema = (toolInputSchema as ToolInputSchema | undefined) ?? {};
|
||||
const schema = (value?.inputSchema as ToolInputSchema | undefined) ?? {};
|
||||
const schemaProperties = isDefined(schema.properties)
|
||||
? (schema.properties as Record<
|
||||
string,
|
||||
@@ -34,9 +38,11 @@ export const SettingsLogicFunctionToolTriggerSection = ({
|
||||
return (
|
||||
<SettingsLogicFunctionTriggerSection
|
||||
title={t`AI tool`}
|
||||
description={t`Triggers the function when called by an AI agent or workflow`}
|
||||
enabled={isTool}
|
||||
onEnabledChange={onChange}
|
||||
description={t`Exposes the function as a tool that AI agents can call`}
|
||||
enabled={isDefined(value)}
|
||||
onEnabledChange={(checked) =>
|
||||
onChange(checked ? DEFAULT_TOOL_TRIGGER_SETTINGS : null)
|
||||
}
|
||||
readonly={readonly}
|
||||
>
|
||||
<SettingsToolParameterTable
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
import { SettingsLogicFunctionTriggerSection } from '@/settings/logic-functions/components/triggers/SettingsLogicFunctionTriggerSection';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { type WorkflowActionTriggerSettings } from 'twenty-shared/application';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
const DEFAULT_WORKFLOW_ACTION_TRIGGER_SETTINGS: WorkflowActionTriggerSettings =
|
||||
{
|
||||
inputSchema: [],
|
||||
};
|
||||
|
||||
type SettingsLogicFunctionWorkflowActionTriggerSectionProps = {
|
||||
value: WorkflowActionTriggerSettings | null;
|
||||
onChange: (value: WorkflowActionTriggerSettings | null) => void;
|
||||
readonly: boolean;
|
||||
};
|
||||
|
||||
export const SettingsLogicFunctionWorkflowActionTriggerSection = ({
|
||||
value,
|
||||
onChange,
|
||||
readonly,
|
||||
}: SettingsLogicFunctionWorkflowActionTriggerSectionProps) => {
|
||||
const { t } = useLingui();
|
||||
|
||||
return (
|
||||
<SettingsLogicFunctionTriggerSection
|
||||
title={t`Workflow action`}
|
||||
description={t`Exposes the function as a step in the workflow builder`}
|
||||
enabled={isDefined(value)}
|
||||
onEnabledChange={(checked) =>
|
||||
onChange(checked ? DEFAULT_WORKFLOW_ACTION_TRIGGER_SETTINGS : null)
|
||||
}
|
||||
readonly={readonly}
|
||||
/>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user