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:
+2
-2
@@ -7,10 +7,10 @@ export const FLAT_LOGIC_FUNCTION_EDITABLE_PROPERTIES = [
|
||||
'checksum',
|
||||
'sourceHandlerPath',
|
||||
'handlerName',
|
||||
'toolInputSchema',
|
||||
'isTool',
|
||||
'cronTriggerSettings',
|
||||
'databaseEventTriggerSettings',
|
||||
'httpRouteTriggerSettings',
|
||||
'toolTriggerSettings',
|
||||
'workflowActionTriggerSettings',
|
||||
'isBuildUpToDate',
|
||||
] as const satisfies MetadataEntityPropertyName<'logicFunction'>[];
|
||||
|
||||
+12
-11
@@ -1,7 +1,6 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
|
||||
import {
|
||||
IsBoolean,
|
||||
IsNotEmpty,
|
||||
IsNumber,
|
||||
IsObject,
|
||||
@@ -16,6 +15,8 @@ import {
|
||||
CronTriggerSettings,
|
||||
DatabaseEventTriggerSettings,
|
||||
HttpRouteTriggerSettings,
|
||||
ToolTriggerSettings,
|
||||
WorkflowActionTriggerSettings,
|
||||
} from 'twenty-shared/application';
|
||||
|
||||
import type { JsonbProperty } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/jsonb-property.type';
|
||||
@@ -51,16 +52,6 @@ export class CreateLogicFunctionFromSourceInput {
|
||||
@IsOptional()
|
||||
timeoutSeconds?: number;
|
||||
|
||||
@Field(() => graphqlTypeJson, { nullable: true })
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
toolInputSchema?: object;
|
||||
|
||||
@IsBoolean()
|
||||
@Field({ nullable: true })
|
||||
@IsOptional()
|
||||
isTool?: boolean;
|
||||
|
||||
@IsObject()
|
||||
@Field(() => graphqlTypeJson, { nullable: true })
|
||||
@IsOptional()
|
||||
@@ -80,4 +71,14 @@ export class CreateLogicFunctionFromSourceInput {
|
||||
@Field(() => graphqlTypeJson, { nullable: true })
|
||||
@IsOptional()
|
||||
httpRouteTriggerSettings?: JsonbProperty<HttpRouteTriggerSettings>;
|
||||
|
||||
@IsObject()
|
||||
@Field(() => graphqlTypeJson, { nullable: true })
|
||||
@IsOptional()
|
||||
toolTriggerSettings?: JsonbProperty<ToolTriggerSettings>;
|
||||
|
||||
@IsObject()
|
||||
@Field(() => graphqlTypeJson, { nullable: true })
|
||||
@IsOptional()
|
||||
workflowActionTriggerSettings?: JsonbProperty<WorkflowActionTriggerSettings>;
|
||||
}
|
||||
|
||||
+1
-6
@@ -1,7 +1,6 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
|
||||
import { IsObject, IsString, Matches } from 'class-validator';
|
||||
import graphqlTypeJson from 'graphql-type-json';
|
||||
import { IsString, Matches } from 'class-validator';
|
||||
|
||||
import { HANDLER_NAME_REGEX } from 'src/engine/metadata-modules/logic-function/constants/handler.contant';
|
||||
|
||||
@@ -11,10 +10,6 @@ export class LogicFunctionSourceInput {
|
||||
@Field({ nullable: false })
|
||||
sourceHandlerCode: string;
|
||||
|
||||
@Field(() => graphqlTypeJson, { nullable: false })
|
||||
@IsObject()
|
||||
toolInputSchema: object;
|
||||
|
||||
@IsString()
|
||||
@Matches(HANDLER_NAME_REGEX, {
|
||||
message: 'handlerName must be a valid JavaScript identifier or dotted path',
|
||||
|
||||
+12
-12
@@ -6,7 +6,6 @@ import {
|
||||
QueryOptions,
|
||||
} from '@ptc-org/nestjs-query-graphql';
|
||||
import {
|
||||
IsBoolean,
|
||||
IsDateString,
|
||||
IsNotEmpty,
|
||||
IsNumber,
|
||||
@@ -20,10 +19,10 @@ import {
|
||||
CronTriggerSettings,
|
||||
DatabaseEventTriggerSettings,
|
||||
HttpRouteTriggerSettings,
|
||||
ToolTriggerSettings,
|
||||
WorkflowActionTriggerSettings,
|
||||
} from 'twenty-shared/application';
|
||||
|
||||
import type { InputJsonSchema } from 'twenty-shared/logic-function';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
|
||||
@ObjectType('LogicFunction')
|
||||
@@ -68,15 +67,6 @@ export class LogicFunctionDTO {
|
||||
@Field()
|
||||
handlerName: string;
|
||||
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
@Field(() => graphqlTypeJson, { nullable: true })
|
||||
toolInputSchema?: InputJsonSchema;
|
||||
|
||||
@IsBoolean()
|
||||
@Field()
|
||||
isTool: boolean;
|
||||
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
@Field(() => graphqlTypeJson, { nullable: true })
|
||||
@@ -92,6 +82,16 @@ export class LogicFunctionDTO {
|
||||
@Field(() => graphqlTypeJson, { nullable: true })
|
||||
httpRouteTriggerSettings?: HttpRouteTriggerSettings;
|
||||
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
@Field(() => graphqlTypeJson, { nullable: true })
|
||||
toolTriggerSettings?: ToolTriggerSettings;
|
||||
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
@Field(() => graphqlTypeJson, { nullable: true })
|
||||
workflowActionTriggerSettings?: WorkflowActionTriggerSettings;
|
||||
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
@Field(() => UUIDScalarType, { nullable: true })
|
||||
|
||||
+12
-11
@@ -2,7 +2,6 @@ import { Field, InputType } from '@nestjs/graphql';
|
||||
|
||||
import { Type } from 'class-transformer';
|
||||
import {
|
||||
IsBoolean,
|
||||
IsNotEmpty,
|
||||
IsNumber,
|
||||
IsObject,
|
||||
@@ -19,6 +18,8 @@ import {
|
||||
CronTriggerSettings,
|
||||
DatabaseEventTriggerSettings,
|
||||
HttpRouteTriggerSettings,
|
||||
ToolTriggerSettings,
|
||||
WorkflowActionTriggerSettings,
|
||||
} from 'twenty-shared/application';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
@@ -49,11 +50,6 @@ class UpdateLogicFunctionFromSourceInputUpdates {
|
||||
@IsOptional()
|
||||
sourceHandlerCode?: string;
|
||||
|
||||
@Field(() => graphqlTypeJson, { nullable: true })
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
toolInputSchema?: object;
|
||||
|
||||
@IsString()
|
||||
@Matches(HANDLER_NAME_REGEX, {
|
||||
message: 'handlerName must be a valid JavaScript identifier or dotted path',
|
||||
@@ -67,11 +63,6 @@ class UpdateLogicFunctionFromSourceInputUpdates {
|
||||
@IsOptional()
|
||||
sourceHandlerPath?: string;
|
||||
|
||||
@IsBoolean()
|
||||
@Field({ nullable: true })
|
||||
@IsOptional()
|
||||
isTool?: boolean;
|
||||
|
||||
@IsObject()
|
||||
@Field(() => graphqlTypeJson, { nullable: true })
|
||||
@IsOptional()
|
||||
@@ -86,6 +77,16 @@ class UpdateLogicFunctionFromSourceInputUpdates {
|
||||
@Field(() => graphqlTypeJson, { nullable: true })
|
||||
@IsOptional()
|
||||
httpRouteTriggerSettings?: JsonbProperty<HttpRouteTriggerSettings>;
|
||||
|
||||
@IsObject()
|
||||
@Field(() => graphqlTypeJson, { nullable: true })
|
||||
@IsOptional()
|
||||
toolTriggerSettings?: JsonbProperty<ToolTriggerSettings>;
|
||||
|
||||
@IsObject()
|
||||
@Field(() => graphqlTypeJson, { nullable: true })
|
||||
@IsOptional()
|
||||
workflowActionTriggerSettings?: JsonbProperty<WorkflowActionTriggerSettings>;
|
||||
}
|
||||
|
||||
@InputType()
|
||||
|
||||
+8
-7
@@ -12,8 +12,9 @@ import {
|
||||
CronTriggerSettings,
|
||||
DatabaseEventTriggerSettings,
|
||||
HttpRouteTriggerSettings,
|
||||
ToolTriggerSettings,
|
||||
WorkflowActionTriggerSettings,
|
||||
} from 'twenty-shared/application';
|
||||
import { type InputJsonSchema } from 'twenty-shared/logic-function';
|
||||
|
||||
import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface';
|
||||
import { type JsonbProperty } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/jsonb-property.type';
|
||||
@@ -59,12 +60,6 @@ export class LogicFunctionEntity
|
||||
@Column({ nullable: true, type: 'text' })
|
||||
checksum: string | null;
|
||||
|
||||
@Column({ nullable: true, type: 'jsonb' })
|
||||
toolInputSchema: JsonbProperty<InputJsonSchema> | null;
|
||||
|
||||
@Column({ nullable: false, default: false })
|
||||
isTool: boolean;
|
||||
|
||||
@Column({ nullable: false, type: 'boolean', default: true })
|
||||
isBuildUpToDate: boolean;
|
||||
|
||||
@@ -77,6 +72,12 @@ export class LogicFunctionEntity
|
||||
@Column({ nullable: true, type: 'jsonb' })
|
||||
httpRouteTriggerSettings: JsonbProperty<HttpRouteTriggerSettings> | null;
|
||||
|
||||
@Column({ nullable: true, type: 'jsonb' })
|
||||
toolTriggerSettings: JsonbProperty<ToolTriggerSettings> | null;
|
||||
|
||||
@Column({ nullable: true, type: 'jsonb' })
|
||||
workflowActionTriggerSettings: JsonbProperty<WorkflowActionTriggerSettings> | null;
|
||||
|
||||
@CreateDateColumn({ type: 'timestamptz' })
|
||||
createdAt: Date;
|
||||
|
||||
|
||||
+3
-5
@@ -4,7 +4,6 @@ import crypto from 'crypto';
|
||||
|
||||
import { v4 } from 'uuid';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { SEED_LOGIC_FUNCTION_INPUT_SCHEMA } from 'twenty-shared/logic-function';
|
||||
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
|
||||
@@ -76,7 +75,6 @@ export class LogicFunctionFromSourceService {
|
||||
builtHandlerPath,
|
||||
handlerName: input.source.handlerName,
|
||||
checksum: null,
|
||||
toolInputSchema: input.source.toolInputSchema,
|
||||
isBuildUpToDate: false,
|
||||
applicationUniversalIdentifier:
|
||||
ownerFlatApplication.universalIdentifier,
|
||||
@@ -111,7 +109,6 @@ export class LogicFunctionFromSourceService {
|
||||
builtHandlerPath,
|
||||
handlerName,
|
||||
checksum,
|
||||
toolInputSchema: SEED_LOGIC_FUNCTION_INPUT_SCHEMA,
|
||||
isBuildUpToDate: true,
|
||||
applicationUniversalIdentifier:
|
||||
ownerFlatApplication.universalIdentifier,
|
||||
@@ -170,8 +167,6 @@ export class LogicFunctionFromSourceService {
|
||||
name: existingLogicFunction.name,
|
||||
description: existingLogicFunction.description,
|
||||
timeoutSeconds: existingLogicFunction.timeoutSeconds,
|
||||
toolInputSchema: existingLogicFunction.toolInputSchema,
|
||||
isTool: existingLogicFunction.isTool,
|
||||
isBuildUpToDate: existingLogicFunction.isBuildUpToDate,
|
||||
checksum: existingLogicFunction.checksum,
|
||||
handlerName: existingLogicFunction.handlerName,
|
||||
@@ -182,6 +177,9 @@ export class LogicFunctionFromSourceService {
|
||||
existingLogicFunction.databaseEventTriggerSettings,
|
||||
httpRouteTriggerSettings:
|
||||
existingLogicFunction.httpRouteTriggerSettings,
|
||||
toolTriggerSettings: existingLogicFunction.toolTriggerSettings,
|
||||
workflowActionTriggerSettings:
|
||||
existingLogicFunction.workflowActionTriggerSettings,
|
||||
applicationUniversalIdentifier:
|
||||
ownerFlatApplication.universalIdentifier,
|
||||
});
|
||||
|
||||
+2
-2
@@ -39,8 +39,6 @@ export const buildUniversalFlatLogicFunctionToCreate = (
|
||||
runtime: LogicFunctionRuntime.NODE22,
|
||||
timeoutSeconds: input.timeoutSeconds ?? 300,
|
||||
checksum: input.checksum ?? null,
|
||||
toolInputSchema: input.toolInputSchema ?? null,
|
||||
isTool: input.isTool ?? false,
|
||||
isBuildUpToDate: input.isBuildUpToDate,
|
||||
handlerName: input.handlerName,
|
||||
sourceHandlerPath: input.sourceHandlerPath,
|
||||
@@ -48,6 +46,8 @@ export const buildUniversalFlatLogicFunctionToCreate = (
|
||||
cronTriggerSettings: input.cronTriggerSettings ?? null,
|
||||
databaseEventTriggerSettings: input.databaseEventTriggerSettings ?? null,
|
||||
httpRouteTriggerSettings: input.httpRouteTriggerSettings ?? null,
|
||||
toolTriggerSettings: input.toolTriggerSettings ?? null,
|
||||
workflowActionTriggerSettings: input.workflowActionTriggerSettings ?? null,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
deletedAt: null,
|
||||
|
||||
+5
-4
@@ -12,7 +12,6 @@ export const fromCreateLogicFunctionFromSourceInputToUniversalFlatLogicFunctionT
|
||||
builtHandlerPath,
|
||||
handlerName,
|
||||
checksum,
|
||||
toolInputSchema,
|
||||
isBuildUpToDate,
|
||||
applicationUniversalIdentifier,
|
||||
}: {
|
||||
@@ -21,7 +20,6 @@ export const fromCreateLogicFunctionFromSourceInputToUniversalFlatLogicFunctionT
|
||||
builtHandlerPath: string;
|
||||
handlerName: string;
|
||||
checksum: string | null;
|
||||
toolInputSchema: object | null;
|
||||
isBuildUpToDate: boolean;
|
||||
applicationUniversalIdentifier: string;
|
||||
}): UniversalFlatLogicFunction & { id: string } => {
|
||||
@@ -44,8 +42,6 @@ export const fromCreateLogicFunctionFromSourceInputToUniversalFlatLogicFunctionT
|
||||
runtime: LogicFunctionRuntime.NODE22,
|
||||
timeoutSeconds: createLogicFunctionFromSourceInput.timeoutSeconds ?? 300,
|
||||
checksum,
|
||||
toolInputSchema,
|
||||
isTool: createLogicFunctionFromSourceInput.isTool ?? false,
|
||||
isBuildUpToDate,
|
||||
handlerName,
|
||||
sourceHandlerPath,
|
||||
@@ -56,6 +52,11 @@ export const fromCreateLogicFunctionFromSourceInputToUniversalFlatLogicFunctionT
|
||||
createLogicFunctionFromSourceInput.databaseEventTriggerSettings ?? null,
|
||||
httpRouteTriggerSettings:
|
||||
createLogicFunctionFromSourceInput.httpRouteTriggerSettings ?? null,
|
||||
toolTriggerSettings:
|
||||
createLogicFunctionFromSourceInput.toolTriggerSettings ?? null,
|
||||
workflowActionTriggerSettings:
|
||||
createLogicFunctionFromSourceInput.workflowActionTriggerSettings ??
|
||||
null,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
deletedAt: null,
|
||||
|
||||
+3
-2
@@ -15,8 +15,6 @@ export const fromFlatLogicFunctionToLogicFunctionDto = ({
|
||||
timeoutSeconds: flatLogicFunction.timeoutSeconds,
|
||||
sourceHandlerPath: flatLogicFunction.sourceHandlerPath,
|
||||
handlerName: flatLogicFunction.handlerName,
|
||||
toolInputSchema: flatLogicFunction.toolInputSchema ?? undefined,
|
||||
isTool: flatLogicFunction.isTool,
|
||||
applicationId: flatLogicFunction.applicationId ?? undefined,
|
||||
workspaceId: flatLogicFunction.workspaceId,
|
||||
createdAt: new Date(flatLogicFunction.createdAt),
|
||||
@@ -26,5 +24,8 @@ export const fromFlatLogicFunctionToLogicFunctionDto = ({
|
||||
flatLogicFunction.databaseEventTriggerSettings ?? undefined,
|
||||
httpRouteTriggerSettings:
|
||||
flatLogicFunction.httpRouteTriggerSettings ?? undefined,
|
||||
toolTriggerSettings: flatLogicFunction.toolTriggerSettings ?? undefined,
|
||||
workflowActionTriggerSettings:
|
||||
flatLogicFunction.workflowActionTriggerSettings ?? undefined,
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user