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:
co-authored by
Claude Opus 4.6
martmull
parent
36452ecc8b
commit
53fdac1417
+9
-1
@@ -10,5 +10,13 @@ export default defineLogicFunction({
|
||||
description: 'Look up a recipient by name to find their details',
|
||||
timeoutSeconds: 5,
|
||||
handler,
|
||||
isTool: true,
|
||||
toolTriggerSettings: {
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
recipientName: { type: 'string' },
|
||||
},
|
||||
required: ['recipientName'],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -111,7 +111,8 @@ export default defineLogicFunction({
|
||||
description:
|
||||
'Structured web search powered by Exa. Returns entity-aware results with category filtering (companies, people, research papers, news, and other content types). Prefer this when the query benefits from structured data or a specific category. For general real-time web browsing, prefer the native `web_search` tool when it is available.',
|
||||
timeoutSeconds: 30,
|
||||
isTool: true,
|
||||
toolInputSchema: exaWebSearchInputSchema,
|
||||
toolTriggerSettings: {
|
||||
inputSchema: exaWebSearchInputSchema,
|
||||
},
|
||||
handler,
|
||||
});
|
||||
|
||||
+18
-17
@@ -10,24 +10,25 @@ export default defineLogicFunction({
|
||||
'Create a Linear issue on behalf of the connected user. Requires a teamId (call list-linear-teams to discover one) and a title.',
|
||||
timeoutSeconds: 30,
|
||||
handler: createLinearIssueHandler,
|
||||
isTool: true,
|
||||
toolInputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
teamId: {
|
||||
type: 'string',
|
||||
description:
|
||||
'The Linear team ID to create the issue in. Use list-linear-teams to discover available teams.',
|
||||
},
|
||||
title: {
|
||||
type: 'string',
|
||||
description: 'The issue title.',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
description: 'Optional issue description (Markdown supported).',
|
||||
toolTriggerSettings: {
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
teamId: {
|
||||
type: 'string',
|
||||
description:
|
||||
'The Linear team ID to create the issue in. Use list-linear-teams to discover available teams.',
|
||||
},
|
||||
title: {
|
||||
type: 'string',
|
||||
description: 'The issue title.',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
description: 'Optional issue description (Markdown supported).',
|
||||
},
|
||||
},
|
||||
required: ['teamId', 'title'],
|
||||
},
|
||||
required: ['teamId', 'title'],
|
||||
},
|
||||
});
|
||||
|
||||
+5
-4
@@ -10,9 +10,10 @@ export default defineLogicFunction({
|
||||
"Returns the connected user's Linear teams. Useful for picking a teamId to pass to create-linear-issue.",
|
||||
timeoutSeconds: 15,
|
||||
handler: listLinearTeamsHandler,
|
||||
isTool: true,
|
||||
toolInputSchema: {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
toolTriggerSettings: {
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -332,11 +332,11 @@ type LogicFunction {
|
||||
timeoutSeconds: Float!
|
||||
sourceHandlerPath: String!
|
||||
handlerName: String!
|
||||
toolInputSchema: JSON
|
||||
isTool: Boolean!
|
||||
cronTriggerSettings: JSON
|
||||
databaseEventTriggerSettings: JSON
|
||||
httpRouteTriggerSettings: JSON
|
||||
toolTriggerSettings: JSON
|
||||
workflowActionTriggerSettings: JSON
|
||||
applicationId: UUID
|
||||
universalIdentifier: UUID
|
||||
createdAt: DateTime!
|
||||
@@ -3779,12 +3779,12 @@ input CreateLogicFunctionFromSourceInput {
|
||||
name: String!
|
||||
description: String
|
||||
timeoutSeconds: Float
|
||||
toolInputSchema: JSON
|
||||
isTool: Boolean
|
||||
source: JSON
|
||||
cronTriggerSettings: JSON
|
||||
databaseEventTriggerSettings: JSON
|
||||
httpRouteTriggerSettings: JSON
|
||||
toolTriggerSettings: JSON
|
||||
workflowActionTriggerSettings: JSON
|
||||
}
|
||||
|
||||
input ExecuteOneLogicFunctionInput {
|
||||
@@ -3808,13 +3808,13 @@ input UpdateLogicFunctionFromSourceInputUpdates {
|
||||
description: String
|
||||
timeoutSeconds: Float
|
||||
sourceHandlerCode: String
|
||||
toolInputSchema: JSON
|
||||
handlerName: String
|
||||
sourceHandlerPath: String
|
||||
isTool: Boolean
|
||||
cronTriggerSettings: JSON
|
||||
databaseEventTriggerSettings: JSON
|
||||
httpRouteTriggerSettings: JSON
|
||||
toolTriggerSettings: JSON
|
||||
workflowActionTriggerSettings: JSON
|
||||
}
|
||||
|
||||
input CreateCommandMenuItemInput {
|
||||
|
||||
@@ -286,11 +286,11 @@ export interface LogicFunction {
|
||||
timeoutSeconds: Scalars['Float']
|
||||
sourceHandlerPath: Scalars['String']
|
||||
handlerName: Scalars['String']
|
||||
toolInputSchema?: Scalars['JSON']
|
||||
isTool: Scalars['Boolean']
|
||||
cronTriggerSettings?: Scalars['JSON']
|
||||
databaseEventTriggerSettings?: Scalars['JSON']
|
||||
httpRouteTriggerSettings?: Scalars['JSON']
|
||||
toolTriggerSettings?: Scalars['JSON']
|
||||
workflowActionTriggerSettings?: Scalars['JSON']
|
||||
applicationId?: Scalars['UUID']
|
||||
universalIdentifier?: Scalars['UUID']
|
||||
createdAt: Scalars['DateTime']
|
||||
@@ -3153,11 +3153,11 @@ export interface LogicFunctionGenqlSelection{
|
||||
timeoutSeconds?: boolean | number
|
||||
sourceHandlerPath?: boolean | number
|
||||
handlerName?: boolean | number
|
||||
toolInputSchema?: boolean | number
|
||||
isTool?: boolean | number
|
||||
cronTriggerSettings?: boolean | number
|
||||
databaseEventTriggerSettings?: boolean | number
|
||||
httpRouteTriggerSettings?: boolean | number
|
||||
toolTriggerSettings?: boolean | number
|
||||
workflowActionTriggerSettings?: boolean | number
|
||||
applicationId?: boolean | number
|
||||
universalIdentifier?: boolean | number
|
||||
createdAt?: boolean | number
|
||||
@@ -6076,7 +6076,7 @@ export interface CreatePageLayoutWidgetInput {pageLayoutTabId: Scalars['UUID'],t
|
||||
|
||||
export interface UpdatePageLayoutWidgetInput {pageLayoutTabId?: (Scalars['UUID'] | null),title?: (Scalars['String'] | null),type?: (WidgetType | null),objectMetadataId?: (Scalars['UUID'] | null),gridPosition?: (GridPositionInput | null),position?: (Scalars['JSON'] | null),configuration?: (Scalars['JSON'] | null),conditionalDisplay?: (Scalars['JSON'] | null),conditionalAvailabilityExpression?: (Scalars['String'] | null)}
|
||||
|
||||
export interface CreateLogicFunctionFromSourceInput {id?: (Scalars['UUID'] | null),universalIdentifier?: (Scalars['UUID'] | null),name: Scalars['String'],description?: (Scalars['String'] | null),timeoutSeconds?: (Scalars['Float'] | null),toolInputSchema?: (Scalars['JSON'] | null),isTool?: (Scalars['Boolean'] | null),source?: (Scalars['JSON'] | null),cronTriggerSettings?: (Scalars['JSON'] | null),databaseEventTriggerSettings?: (Scalars['JSON'] | null),httpRouteTriggerSettings?: (Scalars['JSON'] | null)}
|
||||
export interface CreateLogicFunctionFromSourceInput {id?: (Scalars['UUID'] | null),universalIdentifier?: (Scalars['UUID'] | null),name: Scalars['String'],description?: (Scalars['String'] | null),timeoutSeconds?: (Scalars['Float'] | null),source?: (Scalars['JSON'] | null),cronTriggerSettings?: (Scalars['JSON'] | null),databaseEventTriggerSettings?: (Scalars['JSON'] | null),httpRouteTriggerSettings?: (Scalars['JSON'] | null),toolTriggerSettings?: (Scalars['JSON'] | null),workflowActionTriggerSettings?: (Scalars['JSON'] | null)}
|
||||
|
||||
export interface ExecuteOneLogicFunctionInput {
|
||||
/** Id of the logic function to execute */
|
||||
@@ -6090,7 +6090,7 @@ id: Scalars['UUID'],
|
||||
/** The logic function updates */
|
||||
update: UpdateLogicFunctionFromSourceInputUpdates}
|
||||
|
||||
export interface UpdateLogicFunctionFromSourceInputUpdates {name?: (Scalars['String'] | null),description?: (Scalars['String'] | null),timeoutSeconds?: (Scalars['Float'] | null),sourceHandlerCode?: (Scalars['String'] | null),toolInputSchema?: (Scalars['JSON'] | null),handlerName?: (Scalars['String'] | null),sourceHandlerPath?: (Scalars['String'] | null),isTool?: (Scalars['Boolean'] | null),cronTriggerSettings?: (Scalars['JSON'] | null),databaseEventTriggerSettings?: (Scalars['JSON'] | null),httpRouteTriggerSettings?: (Scalars['JSON'] | null)}
|
||||
export interface UpdateLogicFunctionFromSourceInputUpdates {name?: (Scalars['String'] | null),description?: (Scalars['String'] | null),timeoutSeconds?: (Scalars['Float'] | null),sourceHandlerCode?: (Scalars['String'] | null),handlerName?: (Scalars['String'] | null),sourceHandlerPath?: (Scalars['String'] | null),cronTriggerSettings?: (Scalars['JSON'] | null),databaseEventTriggerSettings?: (Scalars['JSON'] | null),httpRouteTriggerSettings?: (Scalars['JSON'] | null),toolTriggerSettings?: (Scalars['JSON'] | null),workflowActionTriggerSettings?: (Scalars['JSON'] | null)}
|
||||
|
||||
export interface CreateCommandMenuItemInput {workflowVersionId?: (Scalars['UUID'] | null),frontComponentId?: (Scalars['UUID'] | null),engineComponentKey: EngineComponentKey,label: Scalars['String'],icon?: (Scalars['String'] | null),shortLabel?: (Scalars['String'] | null),position?: (Scalars['Float'] | null),isPinned?: (Scalars['Boolean'] | null),availabilityType?: (CommandMenuItemAvailabilityType | null),hotKeys?: (Scalars['String'][] | null),conditionalAvailabilityExpression?: (Scalars['String'] | null),availabilityObjectMetadataId?: (Scalars['UUID'] | null),payload?: (Scalars['JSON'] | null),pageLayoutId?: (Scalars['UUID'] | null)}
|
||||
|
||||
|
||||
@@ -730,12 +730,6 @@ export default {
|
||||
"handlerName": [
|
||||
1
|
||||
],
|
||||
"toolInputSchema": [
|
||||
15
|
||||
],
|
||||
"isTool": [
|
||||
6
|
||||
],
|
||||
"cronTriggerSettings": [
|
||||
15
|
||||
],
|
||||
@@ -745,6 +739,12 @@ export default {
|
||||
"httpRouteTriggerSettings": [
|
||||
15
|
||||
],
|
||||
"toolTriggerSettings": [
|
||||
15
|
||||
],
|
||||
"workflowActionTriggerSettings": [
|
||||
15
|
||||
],
|
||||
"applicationId": [
|
||||
3
|
||||
],
|
||||
@@ -9650,12 +9650,6 @@ export default {
|
||||
"timeoutSeconds": [
|
||||
11
|
||||
],
|
||||
"toolInputSchema": [
|
||||
15
|
||||
],
|
||||
"isTool": [
|
||||
6
|
||||
],
|
||||
"source": [
|
||||
15
|
||||
],
|
||||
@@ -9668,6 +9662,12 @@ export default {
|
||||
"httpRouteTriggerSettings": [
|
||||
15
|
||||
],
|
||||
"toolTriggerSettings": [
|
||||
15
|
||||
],
|
||||
"workflowActionTriggerSettings": [
|
||||
15
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
]
|
||||
@@ -9707,18 +9707,12 @@ export default {
|
||||
"sourceHandlerCode": [
|
||||
1
|
||||
],
|
||||
"toolInputSchema": [
|
||||
15
|
||||
],
|
||||
"handlerName": [
|
||||
1
|
||||
],
|
||||
"sourceHandlerPath": [
|
||||
1
|
||||
],
|
||||
"isTool": [
|
||||
6
|
||||
],
|
||||
"cronTriggerSettings": [
|
||||
15
|
||||
],
|
||||
@@ -9728,6 +9722,12 @@ export default {
|
||||
"httpRouteTriggerSettings": [
|
||||
15
|
||||
],
|
||||
"toolTriggerSettings": [
|
||||
15
|
||||
],
|
||||
"workflowActionTriggerSettings": [
|
||||
15
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
]
|
||||
|
||||
@@ -142,11 +142,14 @@ const handler = async (event: RoutePayload) => {
|
||||
Header names are normalized to lowercase. Access them using lowercase keys (e.g., `event.headers['content-type']`).
|
||||
</Note>
|
||||
|
||||
#### Exposing a function as a tool
|
||||
#### Exposing a function as an AI tool or workflow action
|
||||
|
||||
Logic functions can be exposed as **tools** for AI agents and workflows. When marked as a tool, a function becomes discoverable by Twenty's AI features and can be used in workflow automations.
|
||||
Logic functions can be exposed on two surfaces, each with its own trigger:
|
||||
|
||||
To mark a logic function as a tool, set `isTool: true`:
|
||||
- **`toolTriggerSettings`** — makes the function discoverable by Twenty's AI features (chat, MCP, function calling). Uses standard JSON Schema, the format LLMs natively understand.
|
||||
- **`workflowActionTriggerSettings`** — makes the function appear as a step in the visual workflow builder. Uses Twenty's rich `InputSchema` so the builder can render proper field editors, variable pickers, and labels.
|
||||
|
||||
A function can opt into one, the other, or both. They sit alongside `cronTriggerSettings`, `databaseEventTriggerSettings`, and `httpRouteTriggerSettings` — same pattern, same shape.
|
||||
|
||||
```ts src/logic-functions/enrich-company.logic-function.ts
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
@@ -176,31 +179,33 @@ export default defineLogicFunction({
|
||||
description: 'Enrich a company record with external data',
|
||||
timeoutSeconds: 10,
|
||||
handler,
|
||||
isTool: true,
|
||||
toolTriggerSettings: {},
|
||||
});
|
||||
```
|
||||
|
||||
Key points:
|
||||
|
||||
- You can combine `isTool` with triggers — a function can be both a tool (callable by AI agents) and triggered by events at the same time.
|
||||
- **`toolInputSchema`** (optional): A JSON Schema object describing the parameters your function accepts. The schema is computed automatically from source code static analysis, but you can set it explicitly:
|
||||
- A function can mix surfaces — declare both `toolTriggerSettings` and `workflowActionTriggerSettings` to expose it in chat AND in the workflow builder.
|
||||
- `toolTriggerSettings.inputSchema` and `workflowActionTriggerSettings.inputSchema` are both optional. When omitted, the manifest builder infers them from the handler source code (JSON Schema for the AI tool, Twenty's `InputSchema` for the workflow action). Provide one explicitly when you want richer typing — for example, with `FieldMetadataType`-aware fields like `CURRENCY` or `RELATION` for the workflow builder, or with `description` fields the AI agent can read:
|
||||
|
||||
```ts
|
||||
export default defineLogicFunction({
|
||||
...,
|
||||
toolInputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
companyName: {
|
||||
type: 'string',
|
||||
description: 'The name of the company to enrich',
|
||||
},
|
||||
domain: {
|
||||
type: 'string',
|
||||
description: 'The company website domain (optional)',
|
||||
toolTriggerSettings: {
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
companyName: {
|
||||
type: 'string',
|
||||
description: 'The name of the company to enrich',
|
||||
},
|
||||
domain: {
|
||||
type: 'string',
|
||||
description: 'The company website domain (optional)',
|
||||
},
|
||||
},
|
||||
required: ['companyName'],
|
||||
},
|
||||
required: ['companyName'],
|
||||
},
|
||||
});
|
||||
```
|
||||
@@ -239,7 +244,7 @@ yarn twenty exec --postInstall
|
||||
```
|
||||
|
||||
Key points:
|
||||
- Post-install functions use `definePostInstallLogicFunction()` — a specialized variant that omits trigger settings (`cronTriggerSettings`, `databaseEventTriggerSettings`, `httpRouteTriggerSettings`, `isTool`).
|
||||
- Post-install functions use `definePostInstallLogicFunction()` — a specialized variant that omits trigger settings (`cronTriggerSettings`, `databaseEventTriggerSettings`, `httpRouteTriggerSettings`, `toolTriggerSettings`, `workflowActionTriggerSettings`).
|
||||
- The handler receives an `InstallPayload` with `{ previousVersion?: string; newVersion: string }` — `newVersion` is the version being installed, and `previousVersion` is the version that was previously installed (or `undefined` on a fresh install). Use these values to distinguish fresh installs from upgrades and to run version-specific migration logic.
|
||||
- **When the hook runs**: on fresh installs only, by default. Pass `shouldRunOnVersionUpgrade: true` if you also want it to run when the app is upgraded from a previous version. When omitted, the flag defaults to `false` and upgrades skip the hook.
|
||||
- **Execution model — async by default, sync opt-in**: the `shouldRunSynchronously` flag controls *how* post-install is executed.
|
||||
|
||||
File diff suppressed because one or more lines are too long
+18
-9
@@ -2,7 +2,7 @@ import { useAtomFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAto
|
||||
import { useSetAtomFamilyState } from '@/ui/utilities/state/jotai/hooks/useSetAtomFamilyState';
|
||||
import { logicFunctionTestDataFamilyState } from '@/workflow/workflow-steps/workflow-actions/code-action/states/logicFunctionTestDataFamilyState';
|
||||
import { useEffect } from 'react';
|
||||
import { type InputJsonSchema } from 'twenty-shared/logic-function';
|
||||
import { jsonSchemaToInputSchema } from 'twenty-shared/logic-function';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { getFunctionInputFromInputSchema } from 'twenty-shared/workflow';
|
||||
import { useLogicFunctionForm } from '@/logic-functions/hooks/useLogicFunctionForm';
|
||||
@@ -14,7 +14,12 @@ export const LogicFunctionTestInputInitEffect = ({
|
||||
}) => {
|
||||
const { logicFunction } = useLogicFunctionForm({ logicFunctionId });
|
||||
|
||||
const toolInputSchema = logicFunction?.toolInputSchema;
|
||||
// Prefer the workflow action schema (already in Twenty's InputSchema form)
|
||||
// and fall back to converting the AI tool's JSON Schema when only that
|
||||
// surface is configured.
|
||||
const workflowInputSchema =
|
||||
logicFunction?.workflowActionTriggerSettings?.inputSchema;
|
||||
const toolJsonSchema = logicFunction?.toolTriggerSettings?.inputSchema;
|
||||
|
||||
const logicFunctionTestData = useAtomFamilyStateValue(
|
||||
logicFunctionTestDataFamilyState,
|
||||
@@ -31,15 +36,18 @@ export const LogicFunctionTestInputInitEffect = ({
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isDefined(toolInputSchema)) {
|
||||
let inputSchema = null;
|
||||
if (isDefined(workflowInputSchema)) {
|
||||
inputSchema = workflowInputSchema;
|
||||
} else if (isDefined(toolJsonSchema)) {
|
||||
inputSchema = jsonSchemaToInputSchema(toolJsonSchema);
|
||||
}
|
||||
|
||||
if (!isDefined(inputSchema)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const schemaArray: InputJsonSchema[] = Array.isArray(toolInputSchema)
|
||||
? toolInputSchema
|
||||
: [toolInputSchema];
|
||||
|
||||
const defaultInput = getFunctionInputFromInputSchema(schemaArray)[0];
|
||||
const defaultInput = getFunctionInputFromInputSchema(inputSchema)[0];
|
||||
|
||||
if (!isDefined(defaultInput)) {
|
||||
return;
|
||||
@@ -51,7 +59,8 @@ export const LogicFunctionTestInputInitEffect = ({
|
||||
shouldInitInput: false,
|
||||
}));
|
||||
}, [
|
||||
toolInputSchema,
|
||||
workflowInputSchema,
|
||||
toolJsonSchema,
|
||||
logicFunctionTestData.shouldInitInput,
|
||||
setLogicFunctionTestData,
|
||||
]);
|
||||
|
||||
+2
-2
@@ -9,11 +9,11 @@ export const LOGIC_FUNCTION_FRAGMENT = gql`
|
||||
timeoutSeconds
|
||||
sourceHandlerPath
|
||||
handlerName
|
||||
toolInputSchema
|
||||
isTool
|
||||
cronTriggerSettings
|
||||
databaseEventTriggerSettings
|
||||
httpRouteTriggerSettings
|
||||
toolTriggerSettings
|
||||
workflowActionTriggerSettings
|
||||
applicationId
|
||||
universalIdentifier
|
||||
createdAt
|
||||
|
||||
+2
-5
@@ -40,15 +40,12 @@ describe('useLogicFunctionUpdateFormState', () => {
|
||||
name: 'name',
|
||||
description: '',
|
||||
sourceHandlerCode: '',
|
||||
isTool: false,
|
||||
timeoutSeconds: 300,
|
||||
toolInputSchema: {
|
||||
properties: {},
|
||||
type: 'object',
|
||||
},
|
||||
cronTriggerSettings: null,
|
||||
databaseEventTriggerSettings: null,
|
||||
httpRouteTriggerSettings: null,
|
||||
toolTriggerSettings: null,
|
||||
workflowActionTriggerSettings: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
import { usePersistLogicFunction } from '@/logic-functions/hooks/usePersistLogicFunction';
|
||||
import {
|
||||
getInputSchemaFromSourceCode,
|
||||
jsonSchemaToInputSchema,
|
||||
type InputJsonSchema,
|
||||
} from 'twenty-shared/logic-function';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
@@ -33,19 +34,32 @@ export const useLogicFunctionForm = ({
|
||||
value: LogicFunctionFormValues[TKey],
|
||||
): Promise<InputJsonSchema | undefined> => {
|
||||
if (key === 'sourceHandlerCode') {
|
||||
const toolInputSchema = await getInputSchemaFromSourceCode(
|
||||
const inferredJsonSchema = await getInputSchemaFromSourceCode(
|
||||
value as LogicFunctionFormValues['sourceHandlerCode'],
|
||||
);
|
||||
|
||||
setFormValues((prevState: LogicFunctionFormValues) => ({
|
||||
...prevState,
|
||||
sourceHandlerCode: value as string,
|
||||
toolInputSchema,
|
||||
// Re-infer schemas for any active surface so they stay in sync
|
||||
// with the source code.
|
||||
toolTriggerSettings: prevState.toolTriggerSettings
|
||||
? {
|
||||
...prevState.toolTriggerSettings,
|
||||
inputSchema: inferredJsonSchema,
|
||||
}
|
||||
: null,
|
||||
workflowActionTriggerSettings: prevState.workflowActionTriggerSettings
|
||||
? {
|
||||
...prevState.workflowActionTriggerSettings,
|
||||
inputSchema: jsonSchemaToInputSchema(inferredJsonSchema),
|
||||
}
|
||||
: null,
|
||||
}));
|
||||
|
||||
await handleSave();
|
||||
|
||||
return toolInputSchema;
|
||||
return inferredJsonSchema;
|
||||
}
|
||||
|
||||
setFormValues((prevState: LogicFunctionFormValues) => ({
|
||||
|
||||
+9
-8
@@ -5,21 +5,22 @@ import {
|
||||
type CronTriggerSettings,
|
||||
type DatabaseEventTriggerSettings,
|
||||
type HttpRouteTriggerSettings,
|
||||
type ToolTriggerSettings,
|
||||
type WorkflowActionTriggerSettings,
|
||||
} from 'twenty-shared/application';
|
||||
import { type LogicFunction } from '~/generated-metadata/graphql';
|
||||
import { useGetLogicFunctionSourceCode } from '@/logic-functions/hooks/useGetLogicFunctionSourceCode';
|
||||
import { DEFAULT_TOOL_INPUT_SCHEMA } from 'twenty-shared/logic-function';
|
||||
|
||||
export type LogicFunctionFormValues = {
|
||||
name: string;
|
||||
description: string;
|
||||
isTool: boolean;
|
||||
timeoutSeconds: number;
|
||||
sourceHandlerCode: string;
|
||||
toolInputSchema?: object;
|
||||
cronTriggerSettings: CronTriggerSettings | null;
|
||||
databaseEventTriggerSettings: DatabaseEventTriggerSettings | null;
|
||||
httpRouteTriggerSettings: HttpRouteTriggerSettings | null;
|
||||
toolTriggerSettings: ToolTriggerSettings | null;
|
||||
workflowActionTriggerSettings: WorkflowActionTriggerSettings | null;
|
||||
};
|
||||
|
||||
type SetLogicFunctionFormValues = Dispatch<
|
||||
@@ -39,13 +40,13 @@ export const useLogicFunctionUpdateFormState = ({
|
||||
const [formValues, setFormValues] = useState<LogicFunctionFormValues>({
|
||||
name: '',
|
||||
description: '',
|
||||
isTool: false,
|
||||
sourceHandlerCode: '',
|
||||
timeoutSeconds: 300,
|
||||
toolInputSchema: DEFAULT_TOOL_INPUT_SCHEMA,
|
||||
cronTriggerSettings: null,
|
||||
databaseEventTriggerSettings: null,
|
||||
httpRouteTriggerSettings: null,
|
||||
toolTriggerSettings: null,
|
||||
workflowActionTriggerSettings: null,
|
||||
});
|
||||
|
||||
const { sourceHandlerCode, loading: logicFunctionSourceCodeLoading } =
|
||||
@@ -64,15 +65,15 @@ export const useLogicFunctionUpdateFormState = ({
|
||||
...prevState,
|
||||
name: logicFunction.name || '',
|
||||
description: logicFunction.description || '',
|
||||
isTool: logicFunction.isTool ?? false,
|
||||
timeoutSeconds: logicFunction.timeoutSeconds ?? 300,
|
||||
toolInputSchema:
|
||||
logicFunction.toolInputSchema || DEFAULT_TOOL_INPUT_SCHEMA,
|
||||
cronTriggerSettings: logicFunction.cronTriggerSettings ?? null,
|
||||
databaseEventTriggerSettings:
|
||||
logicFunction.databaseEventTriggerSettings ?? null,
|
||||
httpRouteTriggerSettings:
|
||||
logicFunction.httpRouteTriggerSettings ?? null,
|
||||
toolTriggerSettings: logicFunction.toolTriggerSettings ?? null,
|
||||
workflowActionTriggerSettings:
|
||||
logicFunction.workflowActionTriggerSettings ?? null,
|
||||
}));
|
||||
}
|
||||
}, [logicFunction]);
|
||||
|
||||
+14
-2
@@ -23,8 +23,20 @@ describe('getLogicFunctionTriggerLabel', () => {
|
||||
expect(getLogicFunctionTriggerLabel({}, {})).toBe('');
|
||||
});
|
||||
|
||||
it('returns AI tool when isTool is set', () => {
|
||||
expect(getLogicFunctionTriggerLabel({ isTool: true })).toBe('AI tool');
|
||||
it('returns AI tool when toolTriggerSettings is set', () => {
|
||||
expect(
|
||||
getLogicFunctionTriggerLabel({
|
||||
toolTriggerSettings: { inputSchema: { type: 'object' } },
|
||||
}),
|
||||
).toBe('AI tool');
|
||||
});
|
||||
|
||||
it('returns Workflow action when workflowActionTriggerSettings is set', () => {
|
||||
expect(
|
||||
getLogicFunctionTriggerLabel({
|
||||
workflowActionTriggerSettings: { inputSchema: [] },
|
||||
}),
|
||||
).toBe('Workflow action');
|
||||
});
|
||||
|
||||
it('returns Cron when cron settings are present', () => {
|
||||
|
||||
+4
-2
@@ -3,10 +3,11 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type LogicFunctionLike = {
|
||||
universalIdentifier?: string | null;
|
||||
isTool?: boolean;
|
||||
cronTriggerSettings?: unknown;
|
||||
httpRouteTriggerSettings?: unknown;
|
||||
databaseEventTriggerSettings?: { eventName?: string } | null;
|
||||
toolTriggerSettings?: unknown;
|
||||
workflowActionTriggerSettings?: unknown;
|
||||
};
|
||||
|
||||
export const getLogicFunctionTriggerLabel = (
|
||||
@@ -28,7 +29,8 @@ export const getLogicFunctionTriggerLabel = (
|
||||
) {
|
||||
return t`Pre-install`;
|
||||
}
|
||||
if (lf.isTool) return t`AI tool`;
|
||||
if (isDefined(lf.toolTriggerSettings)) return t`AI tool`;
|
||||
if (isDefined(lf.workflowActionTriggerSettings)) return t`Workflow action`;
|
||||
if (lf.cronTriggerSettings) return t`Cron`;
|
||||
if (lf.httpRouteTriggerSettings) return t`HTTP`;
|
||||
if (lf.databaseEventTriggerSettings) {
|
||||
|
||||
+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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+4
-1
@@ -11,6 +11,7 @@ import { HUMAN_INPUT_ACTIONS } from '@/workflow/workflow-steps/workflow-actions/
|
||||
import { RECORD_ACTIONS } from '@/workflow/workflow-steps/workflow-actions/constants/RecordActions';
|
||||
import { getActionIconColorOrThrow } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIconColorOrThrow';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IconFunction } from 'twenty-ui/display';
|
||||
import { MenuItem } from 'twenty-ui/navigation';
|
||||
|
||||
@@ -28,7 +29,9 @@ export const SidePanelWorkflowSelectAction = ({
|
||||
|
||||
const logicFunctions = useAtomStateValue(logicFunctionsSelector);
|
||||
|
||||
const toolFunctions = logicFunctions.filter((fn) => fn.isTool === true);
|
||||
const toolFunctions = logicFunctions.filter((fn) =>
|
||||
isDefined(fn.workflowActionTriggerSettings),
|
||||
);
|
||||
|
||||
const handleActionClick = (actionType: WorkflowActionType) => {
|
||||
onActionSelected({ type: actionType });
|
||||
|
||||
+7
-8
@@ -38,6 +38,7 @@ import { Key } from 'ts-key-enum';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
getOutputSchemaFromValue,
|
||||
jsonSchemaToInputSchema,
|
||||
type InputJsonSchema,
|
||||
} from 'twenty-shared/logic-function';
|
||||
import { IconCode, IconPlayerPlay } from 'twenty-ui/display';
|
||||
@@ -137,7 +138,7 @@ export const WorkflowEditActionCode = ({
|
||||
);
|
||||
|
||||
const handleUpdateFunctionInputSchema = useDebouncedCallback(
|
||||
async (sourceCode: string, toolInputSchema: InputJsonSchema) => {
|
||||
async (sourceCode: string, inferredJsonSchema: InputJsonSchema) => {
|
||||
if (actionOptions.readonly === true) {
|
||||
return;
|
||||
}
|
||||
@@ -146,11 +147,9 @@ export const WorkflowEditActionCode = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const schemaArray = Array.isArray(toolInputSchema)
|
||||
? toolInputSchema
|
||||
: [toolInputSchema];
|
||||
const inputSchema = jsonSchemaToInputSchema(inferredJsonSchema);
|
||||
|
||||
const newFunctionInput = getFunctionInputFromInputSchema(schemaArray)[0];
|
||||
const newFunctionInput = getFunctionInputFromInputSchema(inputSchema)[0];
|
||||
|
||||
const newMergedInput = mergeDefaultFunctionInputAndFunctionInput({
|
||||
newInput: newFunctionInput,
|
||||
@@ -260,12 +259,12 @@ export const WorkflowEditActionCode = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const toolInputSchema = await onChange('sourceHandlerCode')(newCode);
|
||||
const inferredJsonSchema = await onChange('sourceHandlerCode')(newCode);
|
||||
|
||||
await getUpdatableWorkflowVersion();
|
||||
|
||||
if (isDefined(toolInputSchema)) {
|
||||
await handleUpdateFunctionInputSchema(newCode, toolInputSchema);
|
||||
if (isDefined(inferredJsonSchema)) {
|
||||
await handleUpdateFunctionInputSchema(newCode, inferredJsonSchema);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+5
-8
@@ -81,17 +81,14 @@ export const WorkflowEditActionLogicFunction = ({
|
||||
);
|
||||
|
||||
const functionInput = useMemo(() => {
|
||||
const toolInputSchema = logicFunction?.toolInputSchema;
|
||||
const inputSchema =
|
||||
logicFunction?.workflowActionTriggerSettings?.inputSchema;
|
||||
|
||||
if (!isDefined(toolInputSchema)) {
|
||||
if (!isDefined(inputSchema)) {
|
||||
return action.settings.input.logicFunctionInput ?? {};
|
||||
}
|
||||
|
||||
const schemaArray = Array.isArray(toolInputSchema)
|
||||
? toolInputSchema
|
||||
: [toolInputSchema];
|
||||
|
||||
const defaultInput = getFunctionInputFromInputSchema(schemaArray)[0];
|
||||
const defaultInput = getFunctionInputFromInputSchema(inputSchema)[0];
|
||||
|
||||
if (!isObject(defaultInput)) {
|
||||
return action.settings.input.logicFunctionInput ?? {};
|
||||
@@ -102,7 +99,7 @@ export const WorkflowEditActionLogicFunction = ({
|
||||
oldInput: action.settings.input.logicFunctionInput ?? {},
|
||||
});
|
||||
}, [
|
||||
logicFunction?.toolInputSchema,
|
||||
logicFunction?.workflowActionTriggerSettings?.inputSchema,
|
||||
action.settings.input.logicFunctionInput,
|
||||
]);
|
||||
|
||||
|
||||
@@ -46,7 +46,9 @@ export const SettingsAI = () => {
|
||||
const result = await createLogicFunction({
|
||||
input: {
|
||||
name: 'new-tool',
|
||||
isTool: true,
|
||||
toolTriggerSettings: {
|
||||
inputSchema: { type: 'object', properties: {} },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ export const SettingsToolDetail = () => {
|
||||
: systemTool?.description;
|
||||
|
||||
const inputSchema = isCustomTool
|
||||
? logicFunction?.toolInputSchema
|
||||
? logicFunction?.toolTriggerSettings?.inputSchema
|
||||
: schemaData?.getToolInputSchema;
|
||||
|
||||
const functionLink = isCustomTool
|
||||
|
||||
@@ -139,7 +139,7 @@ export const SettingsToolsTable = () => {
|
||||
const allTools: ToolItem[] = useMemo(
|
||||
() => [
|
||||
...logicFunctions
|
||||
.filter((fn) => fn.isTool === true)
|
||||
.filter((fn) => isDefined(fn.toolTriggerSettings))
|
||||
.map((fn) => ({
|
||||
identifier: fn.id,
|
||||
name: fn.name,
|
||||
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { SettingsRadioCardContainer } from '@/settings/components/SettingsRadioCardContainer';
|
||||
import { useModal } from '@/ui/layout/modal/hooks/useModal';
|
||||
import { H1Title, H1TitleFontColor } from 'twenty-ui/display';
|
||||
import { SectionAlignment, SectionFontColor } from 'twenty-ui/layout';
|
||||
import {
|
||||
StyledAppModal,
|
||||
StyledAppModalButton,
|
||||
StyledAppModalSection,
|
||||
StyledAppModalTitle,
|
||||
} from '~/pages/settings/applications/components/SettingsAppModalLayout';
|
||||
|
||||
type SettingsApplicationConnectScopePickerModalProps = {
|
||||
modalInstanceId: string;
|
||||
providerDisplayName: string;
|
||||
onConfirm: (scope: 'user' | 'workspace') => void;
|
||||
};
|
||||
|
||||
export const SettingsApplicationConnectScopePickerModal = ({
|
||||
modalInstanceId,
|
||||
providerDisplayName,
|
||||
onConfirm,
|
||||
}: SettingsApplicationConnectScopePickerModalProps) => {
|
||||
const { t } = useLingui();
|
||||
const { closeModal } = useModal();
|
||||
const [scope, setScope] = useState<'user' | 'workspace'>('user');
|
||||
|
||||
const options = [
|
||||
{
|
||||
value: 'user',
|
||||
title: t`Just for me`,
|
||||
description: t`Only you can use this credential.`,
|
||||
},
|
||||
{
|
||||
value: 'workspace',
|
||||
title: t`Workspace shared`,
|
||||
description: t`Anyone in this workspace can use this credential.`,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<StyledAppModal modalId={modalInstanceId} isClosable padding="large">
|
||||
<StyledAppModalTitle>
|
||||
<H1Title
|
||||
title={t`Connect ${providerDisplayName}`}
|
||||
fontColor={H1TitleFontColor.Primary}
|
||||
/>
|
||||
</StyledAppModalTitle>
|
||||
<StyledAppModalSection
|
||||
alignment={SectionAlignment.Center}
|
||||
fontColor={SectionFontColor.Primary}
|
||||
>
|
||||
<SettingsRadioCardContainer
|
||||
value={scope}
|
||||
options={options}
|
||||
onChange={(value) => setScope(value as 'user' | 'workspace')}
|
||||
/>
|
||||
</StyledAppModalSection>
|
||||
<StyledAppModalButton
|
||||
onClick={() => closeModal(modalInstanceId)}
|
||||
variant="secondary"
|
||||
title={t`Cancel`}
|
||||
fullWidth
|
||||
justify="center"
|
||||
/>
|
||||
<StyledAppModalButton
|
||||
onClick={() => {
|
||||
closeModal(modalInstanceId);
|
||||
onConfirm(scope);
|
||||
}}
|
||||
variant="secondary"
|
||||
accent="blue"
|
||||
title={t`Continue`}
|
||||
fullWidth
|
||||
justify="center"
|
||||
/>
|
||||
</StyledAppModal>
|
||||
);
|
||||
};
|
||||
-1
@@ -295,7 +295,6 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
sourceHandlerPath: 'my.function.ts',
|
||||
builtHandlerPath: 'my.function.mjs',
|
||||
builtHandlerChecksum: '[checksum]',
|
||||
toolInputSchema: { type: 'object', properties: {} },
|
||||
},
|
||||
],
|
||||
frontComponents: [
|
||||
|
||||
+8
-11
@@ -1597,7 +1597,6 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
handlerName: 'default.config.handler',
|
||||
name: 'root-function',
|
||||
sourceHandlerPath: 'src/root.function.ts',
|
||||
toolInputSchema: { type: 'object', properties: {} },
|
||||
timeoutSeconds: 5,
|
||||
httpRouteTriggerSettings: {
|
||||
httpMethod: 'GET',
|
||||
@@ -1612,7 +1611,6 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
handlerName: 'default.config.handler',
|
||||
name: 'greeting-function',
|
||||
sourceHandlerPath: 'src/logic-functions/greeting.function.ts',
|
||||
toolInputSchema: { type: 'object', properties: {} },
|
||||
timeoutSeconds: 5,
|
||||
httpRouteTriggerSettings: {
|
||||
httpMethod: 'GET',
|
||||
@@ -1626,16 +1624,18 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
builtHandlerPath: 'src/logic-functions/lookup-recipient.function.mjs',
|
||||
description: 'Look up a recipient by name to find their details',
|
||||
handlerName: 'default.config.handler',
|
||||
isTool: true,
|
||||
name: 'lookup-recipient',
|
||||
sourceHandlerPath: 'src/logic-functions/lookup-recipient.function.ts',
|
||||
timeoutSeconds: 5,
|
||||
toolInputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
recipientName: {
|
||||
type: 'string',
|
||||
toolTriggerSettings: {
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
recipientName: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
required: ['recipientName'],
|
||||
},
|
||||
},
|
||||
universalIdentifier: 'a1b2c3d4-1001-4a7b-8c9d-0e1f2a3b4c5d',
|
||||
@@ -1651,7 +1651,6 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
name: 'on-post-card-created',
|
||||
sourceHandlerPath: 'src/logic-functions/on-post-card-created.function.ts',
|
||||
timeoutSeconds: 5,
|
||||
toolInputSchema: { type: 'object', properties: {} },
|
||||
universalIdentifier: 'a1b2c3d4-db01-4a7b-8c9d-0e1f2a3b4c5d',
|
||||
},
|
||||
{
|
||||
@@ -1660,7 +1659,6 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
handlerName: 'default.config.handler',
|
||||
name: 'test-function-2',
|
||||
sourceHandlerPath: 'src/logic-functions/test-function-2.function.ts',
|
||||
toolInputSchema: { type: 'object', properties: {} },
|
||||
timeoutSeconds: 2,
|
||||
cronTriggerSettings: {
|
||||
pattern: '0 0 1 1 *',
|
||||
@@ -1673,7 +1671,6 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
handlerName: 'default.config.handler',
|
||||
name: 'test-function',
|
||||
sourceHandlerPath: 'src/logic-functions/test-function.function.ts',
|
||||
toolInputSchema: { type: 'object', properties: {} },
|
||||
timeoutSeconds: 2,
|
||||
httpRouteTriggerSettings: {
|
||||
forwardedRequestHeaders: ['signature'],
|
||||
|
||||
@@ -31,13 +31,16 @@ import {
|
||||
type ObjectManifest,
|
||||
type PageLayoutManifest,
|
||||
type PageLayoutTabManifest,
|
||||
type PostInstallLogicFunctionApplicationManifest,
|
||||
type PreInstallLogicFunctionApplicationManifest,
|
||||
type RoleManifest,
|
||||
type SkillManifest,
|
||||
type ViewManifest,
|
||||
type PostInstallLogicFunctionApplicationManifest,
|
||||
type PreInstallLogicFunctionApplicationManifest,
|
||||
} from 'twenty-shared/application';
|
||||
import { getInputSchemaFromSourceCode } from 'twenty-shared/logic-function';
|
||||
import {
|
||||
getInputSchemaFromSourceCode,
|
||||
jsonSchemaToInputSchema,
|
||||
} from 'twenty-shared/logic-function';
|
||||
import { assertUnreachable } from 'twenty-shared/utils';
|
||||
import { addMissingFieldOptionIds } from '@/cli/utilities/build/manifest/utils/add-missing-field-option-ids';
|
||||
import { type PostInstallLogicFunctionConfig } from '@/sdk/define/logic-functions/post-install-logic-function-config';
|
||||
@@ -234,13 +237,43 @@ export const buildManifest = async (
|
||||
|
||||
const relativeFilePath = relative(appPath, filePath);
|
||||
|
||||
const toolInputSchema =
|
||||
rest.toolInputSchema ??
|
||||
(await getInputSchemaFromSourceCode(fileContent));
|
||||
// Auto-infer inputSchema for any trigger that opts in but omits one.
|
||||
// For the AI tool surface we use the JSON schema directly; for the
|
||||
// workflow action surface we convert to Twenty's InputSchema.
|
||||
const inferredJsonSchema =
|
||||
(rest.toolTriggerSettings && !rest.toolTriggerSettings.inputSchema) ||
|
||||
(rest.workflowActionTriggerSettings &&
|
||||
!rest.workflowActionTriggerSettings.inputSchema)
|
||||
? await getInputSchemaFromSourceCode(fileContent)
|
||||
: null;
|
||||
|
||||
const toolTriggerSettings = rest.toolTriggerSettings
|
||||
? {
|
||||
...rest.toolTriggerSettings,
|
||||
inputSchema:
|
||||
rest.toolTriggerSettings.inputSchema ??
|
||||
inferredJsonSchema ??
|
||||
undefined,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const workflowActionTriggerSettings = rest.workflowActionTriggerSettings
|
||||
? {
|
||||
...rest.workflowActionTriggerSettings,
|
||||
inputSchema:
|
||||
rest.workflowActionTriggerSettings.inputSchema ??
|
||||
(inferredJsonSchema
|
||||
? jsonSchemaToInputSchema(inferredJsonSchema)
|
||||
: undefined),
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const config: LogicFunctionManifest = {
|
||||
...rest,
|
||||
toolInputSchema,
|
||||
...(toolTriggerSettings ? { toolTriggerSettings } : {}),
|
||||
...(workflowActionTriggerSettings
|
||||
? { workflowActionTriggerSettings }
|
||||
: {}),
|
||||
handlerName: 'default.config.handler',
|
||||
sourceHandlerPath: relativeFilePath,
|
||||
builtHandlerPath: relativeFilePath.replace(/\.tsx?$/, '.mjs'),
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { type LogicFunctionManifest } from 'twenty-shared/application';
|
||||
import { type InputJsonSchema } from 'twenty-shared/logic-function';
|
||||
|
||||
export type LogicFunctionHandler = (...args: any[]) => any | Promise<any>;
|
||||
|
||||
@@ -9,8 +8,6 @@ export type LogicFunctionConfig = Omit<
|
||||
| 'builtHandlerPath'
|
||||
| 'builtHandlerChecksum'
|
||||
| 'handlerName'
|
||||
| 'toolInputSchema'
|
||||
> & {
|
||||
handler: LogicFunctionHandler;
|
||||
toolInputSchema?: InputJsonSchema;
|
||||
};
|
||||
|
||||
+2
-1
@@ -6,7 +6,8 @@ export type PreInstallLogicFunctionConfig = Omit<
|
||||
| 'cronTriggerSettings'
|
||||
| 'databaseEventTriggerSettings'
|
||||
| 'httpRouteTriggerSettings'
|
||||
| 'isTool'
|
||||
| 'toolTriggerSettings'
|
||||
| 'workflowActionTriggerSettings'
|
||||
| 'handler'
|
||||
> & {
|
||||
handler: InstallHandler;
|
||||
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
import { QueryRunner } from 'typeorm';
|
||||
|
||||
import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
|
||||
import { FastInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/fast-instance-command.interface';
|
||||
|
||||
@RegisteredInstanceCommand('2.3.0', 1797000001000)
|
||||
export class AddToolAndWorkflowActionTriggerSettingsFastInstanceCommand
|
||||
implements FastInstanceCommand
|
||||
{
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."logicFunction" ADD "toolTriggerSettings" jsonb`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."logicFunction" ADD "workflowActionTriggerSettings" jsonb`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."logicFunction" DROP COLUMN "workflowActionTriggerSettings"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."logicFunction" DROP COLUMN "toolTriggerSettings"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
import { DataSource, QueryRunner } from 'typeorm';
|
||||
|
||||
import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
|
||||
import { SlowInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/slow-instance-command.interface';
|
||||
|
||||
// isTool=true previously exposed a function on both surfaces (AI tool and
|
||||
// workflow node), so we populate both new triggers when migrating.
|
||||
@RegisteredInstanceCommand('2.3.0', 1797000002000, { type: 'slow' })
|
||||
export class MigrateToolTriggerSettingsSlowInstanceCommand
|
||||
implements SlowInstanceCommand
|
||||
{
|
||||
async runDataMigration(dataSource: DataSource): Promise<void> {
|
||||
const defaultJsonSchema = `'{"type":"object","properties":{}}'::jsonb`;
|
||||
|
||||
await dataSource.query(
|
||||
`UPDATE "core"."logicFunction"
|
||||
SET "toolTriggerSettings" = jsonb_build_object(
|
||||
'inputSchema',
|
||||
COALESCE("toolInputSchema", ${defaultJsonSchema})
|
||||
),
|
||||
"workflowActionTriggerSettings" = jsonb_build_object(
|
||||
'inputSchema',
|
||||
jsonb_build_array(
|
||||
COALESCE("toolInputSchema", ${defaultJsonSchema})
|
||||
)
|
||||
)
|
||||
WHERE "isTool" = true
|
||||
AND "toolTriggerSettings" IS NULL
|
||||
AND "workflowActionTriggerSettings" IS NULL`,
|
||||
);
|
||||
}
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."logicFunction" DROP COLUMN "isTool"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."logicFunction" DROP COLUMN "toolInputSchema"`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."logicFunction" ADD "toolInputSchema" jsonb`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."logicFunction" ADD "isTool" boolean NOT NULL DEFAULT false`,
|
||||
);
|
||||
// Best-effort reverse backfill so existing tools keep functioning if
|
||||
// someone rolls back. Pulls the JSON schema out of toolTriggerSettings.
|
||||
await queryRunner.query(
|
||||
`UPDATE "core"."logicFunction"
|
||||
SET "isTool" = true,
|
||||
"toolInputSchema" = "toolTriggerSettings"->'inputSchema'
|
||||
WHERE "toolTriggerSettings" IS NOT NULL`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+4
@@ -22,6 +22,8 @@ import { AddUpgradeMigrationWorkspaceIdIndexFastInstanceCommand } from 'src/data
|
||||
import { AddCacheTokensToAgentChatThreadFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-2/2-2-instance-command-fast-1777455269302-add-cache-tokens-to-agent-chat-thread';
|
||||
import { AddLogoToApplicationFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-2/2-2-instance-command-fast-1777539664664-add-logo-to-application';
|
||||
import { AddDeletedAtToAgentChatThreadFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-3/2-3-instance-command-fast-1777682000000-add-deleted-at-to-agent-chat-thread';
|
||||
import { AddToolAndWorkflowActionTriggerSettingsFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-3/2-3-instance-command-fast-1797000001000-add-tool-and-workflow-action-trigger-settings';
|
||||
import { MigrateToolTriggerSettingsSlowInstanceCommand } from 'src/database/commands/upgrade-version-command/2-3/2-3-instance-command-slow-1797000002000-migrate-tool-trigger-settings';
|
||||
import { ConnectionProviderSyncableEntityFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-3/2-3-instance-command-fast-1777896012579-connection-provider-syncable-entity';
|
||||
import { RemoveUserDefaultAvatarUrlFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-3/2-3-instance-command-fast-1777915958318-remove-user-default-avatar-url';
|
||||
|
||||
@@ -48,6 +50,8 @@ export const INSTANCE_COMMANDS = [
|
||||
AddCacheTokensToAgentChatThreadFastInstanceCommand,
|
||||
AddLogoToApplicationFastInstanceCommand,
|
||||
AddDeletedAtToAgentChatThreadFastInstanceCommand,
|
||||
AddToolAndWorkflowActionTriggerSettingsFastInstanceCommand,
|
||||
MigrateToolTriggerSettingsSlowInstanceCommand,
|
||||
ConnectionProviderSyncableEntityFastInstanceCommand,
|
||||
RemoveUserDefaultAvatarUrlFastInstanceCommand,
|
||||
];
|
||||
|
||||
+3
-2
@@ -28,13 +28,14 @@ export const fromLogicFunctionManifestToUniversalFlatLogicFunction = ({
|
||||
builtHandlerPath: logicFunctionManifest.builtHandlerPath,
|
||||
handlerName: logicFunctionManifest.handlerName,
|
||||
checksum: logicFunctionManifest.builtHandlerChecksum,
|
||||
toolInputSchema: logicFunctionManifest.toolInputSchema,
|
||||
isTool: logicFunctionManifest.isTool ?? false,
|
||||
cronTriggerSettings: logicFunctionManifest.cronTriggerSettings ?? null,
|
||||
databaseEventTriggerSettings:
|
||||
logicFunctionManifest.databaseEventTriggerSettings ?? null,
|
||||
httpRouteTriggerSettings:
|
||||
logicFunctionManifest.httpRouteTriggerSettings ?? null,
|
||||
toolTriggerSettings: logicFunctionManifest.toolTriggerSettings ?? null,
|
||||
workflowActionTriggerSettings:
|
||||
logicFunctionManifest.workflowActionTriggerSettings ?? null,
|
||||
isBuildUpToDate: true,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
// Refresh result returned by the app OAuth driver. Mirrors
|
||||
// `ConnectedAccountTokens` from the central refresh manager but redeclared
|
||||
// here so this engine-side driver has zero dependency on `modules/`.
|
||||
|
||||
export type AppOAuthTokens = {
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
};
|
||||
+42
@@ -42,6 +42,48 @@ export class ApplicationVariableEntityService {
|
||||
});
|
||||
}
|
||||
|
||||
// Decrypted plaintext value. Server-side only — never expose via GraphQL.
|
||||
// Used by trusted server flows that need the raw secret (e.g. exchanging an
|
||||
// OAuth client secret with a third-party provider).
|
||||
getRawValue(applicationVariable: ApplicationVariableEntity): string {
|
||||
if (!applicationVariable.isSecret) {
|
||||
return applicationVariable.value;
|
||||
}
|
||||
|
||||
return this.secretEncryptionService.decrypt(applicationVariable.value);
|
||||
}
|
||||
|
||||
async findOneByKey({
|
||||
applicationId,
|
||||
key,
|
||||
}: {
|
||||
applicationId: string;
|
||||
key: string;
|
||||
}): Promise<ApplicationVariableEntity | null> {
|
||||
return this.applicationVariableRepository.findOne({
|
||||
where: { applicationId, key },
|
||||
});
|
||||
}
|
||||
|
||||
async getRawValueByKeyOrThrow({
|
||||
applicationId,
|
||||
key,
|
||||
}: {
|
||||
applicationId: string;
|
||||
key: string;
|
||||
}): Promise<string> {
|
||||
const variable = await this.findOneByKey({ applicationId, key });
|
||||
|
||||
if (!isDefined(variable)) {
|
||||
throw new ApplicationVariableEntityException(
|
||||
`Application variable "${key}" not found for application ${applicationId}`,
|
||||
ApplicationVariableEntityExceptionCode.APPLICATION_VARIABLE_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
return this.getRawValue(variable);
|
||||
}
|
||||
|
||||
async update({
|
||||
key,
|
||||
plainTextValue,
|
||||
|
||||
+9
-7
@@ -58,7 +58,9 @@ export class LogicFunctionToolProvider implements ToolProvider {
|
||||
flatLogicFunctionMaps.byUniversalIdentifier,
|
||||
).filter(
|
||||
(fn): fn is FlatLogicFunction =>
|
||||
isDefined(fn) && fn.isTool === true && fn.deletedAt === null,
|
||||
isDefined(fn) &&
|
||||
isDefined(fn.toolTriggerSettings) &&
|
||||
fn.deletedAt === null,
|
||||
);
|
||||
|
||||
const descriptors: (ToolIndexEntry | ToolDescriptor)[] = [];
|
||||
@@ -79,12 +81,12 @@ export class LogicFunctionToolProvider implements ToolProvider {
|
||||
};
|
||||
|
||||
if (includeSchemas) {
|
||||
// Logic functions already store JSON Schema -- use it directly
|
||||
const inputSchema =
|
||||
(logicFunction.toolInputSchema as object) ??
|
||||
DEFAULT_TOOL_INPUT_SCHEMA;
|
||||
|
||||
descriptors.push({ ...base, inputSchema });
|
||||
descriptors.push({
|
||||
...base,
|
||||
inputSchema:
|
||||
(logicFunction.toolTriggerSettings?.inputSchema as object) ??
|
||||
DEFAULT_TOOL_INPUT_SCHEMA,
|
||||
});
|
||||
} else {
|
||||
descriptors.push(base);
|
||||
}
|
||||
|
||||
+4
-3
@@ -114,20 +114,21 @@ exports[`ALL_UNIVERSAL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY should ma
|
||||
"checksum",
|
||||
"sourceHandlerPath",
|
||||
"handlerName",
|
||||
"toolInputSchema",
|
||||
"isTool",
|
||||
"isBuildUpToDate",
|
||||
"deletedAt",
|
||||
"cronTriggerSettings",
|
||||
"databaseEventTriggerSettings",
|
||||
"httpRouteTriggerSettings",
|
||||
"toolTriggerSettings",
|
||||
"workflowActionTriggerSettings",
|
||||
"builtHandlerPath",
|
||||
],
|
||||
"propertiesToStringify": [
|
||||
"toolInputSchema",
|
||||
"cronTriggerSettings",
|
||||
"databaseEventTriggerSettings",
|
||||
"httpRouteTriggerSettings",
|
||||
"toolTriggerSettings",
|
||||
"workflowActionTriggerSettings",
|
||||
],
|
||||
},
|
||||
"navigationMenuItem": {
|
||||
|
||||
+10
-10
@@ -576,16 +576,6 @@ export const ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME = {
|
||||
toStringify: false,
|
||||
universalProperty: undefined,
|
||||
},
|
||||
toolInputSchema: {
|
||||
toCompare: true,
|
||||
toStringify: true,
|
||||
universalProperty: undefined,
|
||||
},
|
||||
isTool: {
|
||||
toCompare: true,
|
||||
toStringify: false,
|
||||
universalProperty: undefined,
|
||||
},
|
||||
isBuildUpToDate: {
|
||||
toCompare: true,
|
||||
toStringify: false,
|
||||
@@ -611,6 +601,16 @@ export const ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME = {
|
||||
toStringify: true,
|
||||
universalProperty: undefined,
|
||||
},
|
||||
toolTriggerSettings: {
|
||||
toCompare: true,
|
||||
toStringify: true,
|
||||
universalProperty: undefined,
|
||||
},
|
||||
workflowActionTriggerSettings: {
|
||||
toCompare: true,
|
||||
toStringify: true,
|
||||
universalProperty: undefined,
|
||||
},
|
||||
createdAt: {
|
||||
toCompare: false,
|
||||
toStringify: false,
|
||||
|
||||
+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,
|
||||
};
|
||||
};
|
||||
|
||||
-2
@@ -45,10 +45,8 @@ export class PrefillLogicFunctionService {
|
||||
id: definition.id,
|
||||
name: definition.name,
|
||||
description: definition.description,
|
||||
toolInputSchema: definition.toolInputSchema,
|
||||
source: {
|
||||
sourceHandlerCode: definition.sourceHandlerCode,
|
||||
toolInputSchema: definition.toolInputSchema,
|
||||
handlerName: 'main',
|
||||
},
|
||||
},
|
||||
|
||||
-40
@@ -20,42 +20,6 @@ export const getCreateCompanyWhenAddingNewPersonCodeStepLogicFunctionIds = (
|
||||
),
|
||||
});
|
||||
|
||||
const EXTRACT_DOMAIN_TOOL_INPUT_SCHEMA = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
email: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
required: ['email'],
|
||||
};
|
||||
|
||||
const IS_PERSONAL_EMAIL_TOOL_INPUT_SCHEMA = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
primaryEmail: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
required: ['primaryEmail'],
|
||||
};
|
||||
|
||||
const FIND_MATCHING_COMPANY_BY_DOMAIN_TOOL_INPUT_SCHEMA = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
companies: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
},
|
||||
},
|
||||
domain: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
required: ['companies', 'domain'],
|
||||
};
|
||||
|
||||
const EXTRACT_DOMAIN_LOGIC_FUNCTION_SOURCE = `const psl = require('psl');
|
||||
|
||||
export const main = async (params) => {
|
||||
@@ -184,7 +148,6 @@ export type PrefilledWorkflowCodeStepLogicFunctionDefinition = {
|
||||
name: string;
|
||||
description: string;
|
||||
sourceHandlerCode: string;
|
||||
toolInputSchema: object;
|
||||
};
|
||||
|
||||
export const getCreateCompanyWhenAddingNewPersonCodeStepLogicFunctionDefinitions =
|
||||
@@ -203,7 +166,6 @@ export const getCreateCompanyWhenAddingNewPersonCodeStepLogicFunctionDefinitions
|
||||
description:
|
||||
'Extracts a normalized company domain and URL from a person email address.',
|
||||
sourceHandlerCode: EXTRACT_DOMAIN_LOGIC_FUNCTION_SOURCE,
|
||||
toolInputSchema: EXTRACT_DOMAIN_TOOL_INPUT_SCHEMA,
|
||||
},
|
||||
{
|
||||
id: findMatchingCompanyByDomainLogicFunctionId,
|
||||
@@ -212,7 +174,6 @@ export const getCreateCompanyWhenAddingNewPersonCodeStepLogicFunctionDefinitions
|
||||
'Finds an existing company whose website matches a normalized registrable domain.',
|
||||
sourceHandlerCode:
|
||||
FIND_MATCHING_COMPANY_BY_DOMAIN_LOGIC_FUNCTION_SOURCE,
|
||||
toolInputSchema: FIND_MATCHING_COMPANY_BY_DOMAIN_TOOL_INPUT_SCHEMA,
|
||||
},
|
||||
{
|
||||
id: isPersonalEmailLogicFunctionId,
|
||||
@@ -220,7 +181,6 @@ export const getCreateCompanyWhenAddingNewPersonCodeStepLogicFunctionDefinitions
|
||||
description:
|
||||
'Detects whether an email address belongs to a common personal email provider.',
|
||||
sourceHandlerCode: IS_PERSONAL_EMAIL_LOGIC_FUNCTION_SOURCE,
|
||||
toolInputSchema: IS_PERSONAL_EMAIL_TOOL_INPUT_SCHEMA,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
+4
-4
@@ -54,8 +54,8 @@ describe('WorkflowVersionStepOperationsWorkspaceService', () => {
|
||||
builtHandlerPath: 'index.mjs',
|
||||
handlerName: 'main',
|
||||
checksum: null,
|
||||
toolInputSchema: null,
|
||||
isTool: false,
|
||||
toolTriggerSettings: null,
|
||||
workflowActionTriggerSettings: null,
|
||||
universalIdentifier: 'universal-id',
|
||||
applicationId: 'application-id',
|
||||
cronTriggerSettings: null,
|
||||
@@ -77,8 +77,8 @@ describe('WorkflowVersionStepOperationsWorkspaceService', () => {
|
||||
builtHandlerPath: 'index.mjs',
|
||||
handlerName: 'main',
|
||||
checksum: null,
|
||||
toolInputSchema: null,
|
||||
isTool: false,
|
||||
toolTriggerSettings: null,
|
||||
workflowActionTriggerSettings: null,
|
||||
universalIdentifier: 'universal-id',
|
||||
applicationId: 'application-id',
|
||||
cronTriggerSettings: null,
|
||||
|
||||
+14
-8
@@ -182,10 +182,13 @@ export class WorkflowVersionStepOperationsWorkspaceService {
|
||||
},
|
||||
input: {
|
||||
logicFunctionId: newLogicFunction.id,
|
||||
logicFunctionInput: isDefined(newLogicFunction.toolInputSchema)
|
||||
? (getFunctionInputFromInputSchema([
|
||||
newLogicFunction.toolInputSchema,
|
||||
])[0] ?? {})
|
||||
logicFunctionInput: isDefined(
|
||||
newLogicFunction.workflowActionTriggerSettings?.inputSchema,
|
||||
)
|
||||
? (getFunctionInputFromInputSchema(
|
||||
newLogicFunction.workflowActionTriggerSettings
|
||||
.inputSchema,
|
||||
)[0] ?? {})
|
||||
: {},
|
||||
},
|
||||
},
|
||||
@@ -235,10 +238,13 @@ export class WorkflowVersionStepOperationsWorkspaceService {
|
||||
},
|
||||
input: {
|
||||
logicFunctionId,
|
||||
logicFunctionInput: isDefined(flatLogicFunction.toolInputSchema)
|
||||
? (getFunctionInputFromInputSchema([
|
||||
flatLogicFunction.toolInputSchema,
|
||||
])[0] ?? {})
|
||||
logicFunctionInput: isDefined(
|
||||
flatLogicFunction.workflowActionTriggerSettings?.inputSchema,
|
||||
)
|
||||
? (getFunctionInputFromInputSchema(
|
||||
flatLogicFunction.workflowActionTriggerSettings
|
||||
.inputSchema,
|
||||
)[0] ?? {})
|
||||
: {},
|
||||
},
|
||||
},
|
||||
|
||||
+8
-1
@@ -1,6 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { resolveInput } from 'twenty-shared/utils';
|
||||
import { isDefined, resolveInput } from 'twenty-shared/utils';
|
||||
|
||||
import { type WorkflowAction } from 'src/modules/workflow/workflow-executor/interfaces/workflow-action.interface';
|
||||
|
||||
@@ -69,6 +69,13 @@ export class LogicFunctionWorkflowAction implements WorkflowAction {
|
||||
);
|
||||
}
|
||||
|
||||
if (!isDefined(logicFunction.workflowActionTriggerSettings)) {
|
||||
throw new WorkflowStepExecutorException(
|
||||
`Logic function ${logicFunction.name} is not exposed as a workflow action`,
|
||||
WorkflowStepExecutorExceptionCode.INVALID_STEP_TYPE,
|
||||
);
|
||||
}
|
||||
|
||||
const result = await this.logicFunctionExecutorService.execute({
|
||||
logicFunctionId: workflowActionInput.logicFunctionId,
|
||||
workspaceId,
|
||||
|
||||
+6
-4
@@ -15,7 +15,7 @@ export const createListLogicFunctionToolsTool = (
|
||||
) => ({
|
||||
name: 'list_logic_function_tools' as const,
|
||||
description:
|
||||
'List all logic functions marked as tools that can be added as LOGIC_FUNCTION steps in workflows. Returns their IDs, names, and descriptions.',
|
||||
'List all logic functions exposed as workflow actions, which can be added as LOGIC_FUNCTION steps in workflows. Returns their IDs, names, and descriptions.',
|
||||
inputSchema: listLogicFunctionToolsSchema,
|
||||
execute: async () => {
|
||||
const { flatLogicFunctionMaps } =
|
||||
@@ -26,16 +26,18 @@ export const createListLogicFunctionToolsTool = (
|
||||
},
|
||||
);
|
||||
|
||||
const toolFunctions = Object.values(
|
||||
const workflowActionFunctions = Object.values(
|
||||
flatLogicFunctionMaps.byUniversalIdentifier,
|
||||
).filter(
|
||||
(fn): fn is FlatLogicFunction =>
|
||||
isDefined(fn) && fn.isTool === true && fn.deletedAt === null,
|
||||
isDefined(fn) &&
|
||||
isDefined(fn.workflowActionTriggerSettings) &&
|
||||
fn.deletedAt === null,
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
logicFunctions: toolFunctions.map((fn) => ({
|
||||
logicFunctions: workflowActionFunctions.map((fn) => ({
|
||||
id: fn.id,
|
||||
name: fn.name,
|
||||
description: fn.description,
|
||||
|
||||
-16
@@ -65,10 +65,6 @@ describe('Logic Function Execution', () => {
|
||||
id: createData.createOneLogicFunction.id,
|
||||
update: {
|
||||
sourceHandlerCode: DEFAULT_TEMPLATE_FUNCTION_CODE,
|
||||
toolInputSchema: {
|
||||
type: 'object',
|
||||
properties: { message: { type: 'string' } },
|
||||
},
|
||||
},
|
||||
},
|
||||
expectToFail: false,
|
||||
@@ -115,10 +111,6 @@ describe('Logic Function Execution', () => {
|
||||
id: createData.createOneLogicFunction.id,
|
||||
update: {
|
||||
sourceHandlerCode: EXTERNAL_PACKAGES_FUNCTION_CODE,
|
||||
toolInputSchema: {
|
||||
type: 'object',
|
||||
properties: { message: { type: 'string' } },
|
||||
},
|
||||
},
|
||||
},
|
||||
expectToFail: false,
|
||||
@@ -207,10 +199,6 @@ describe('Logic Function Execution', () => {
|
||||
name: 'Test Default Function',
|
||||
source: {
|
||||
sourceHandlerCode: DEFAULT_TEMPLATE_FUNCTION_CODE,
|
||||
toolInputSchema: {
|
||||
type: 'object',
|
||||
properties: { message: { type: 'string' } },
|
||||
},
|
||||
handlerName: 'main',
|
||||
},
|
||||
},
|
||||
@@ -263,10 +251,6 @@ describe('Logic Function Execution', () => {
|
||||
id: createData.createOneLogicFunction.id,
|
||||
update: {
|
||||
sourceHandlerCode: ERROR_FUNCTION_CODE,
|
||||
toolInputSchema: {
|
||||
type: 'object',
|
||||
properties: { message: { type: 'string' } },
|
||||
},
|
||||
},
|
||||
},
|
||||
expectToFail: false,
|
||||
|
||||
@@ -63,6 +63,7 @@ export type { ServerVariables } from './server-variables.type';
|
||||
export type { SkillManifest } from './skillManifestType';
|
||||
export type { StoredOAuthConnectionProviderConfig } from './storedOAuthConnectionProviderConfigType';
|
||||
export type { SyncableEntityOptions } from './syncableEntityOptionsType';
|
||||
export type { ToolTriggerSettings } from './toolTriggerSettingsType';
|
||||
export type {
|
||||
ViewManifestFilterValue,
|
||||
ViewFieldManifest,
|
||||
@@ -73,3 +74,4 @@ export type {
|
||||
ViewSortManifest,
|
||||
ViewManifest,
|
||||
} from './viewManifestType';
|
||||
export type { WorkflowActionTriggerSettings } from './workflowActionTriggerSettingsType';
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { type SyncableEntityOptions } from '@/application/syncableEntityOptionsType';
|
||||
import { type ToolTriggerSettings } from '@/application/toolTriggerSettingsType';
|
||||
import { type WorkflowActionTriggerSettings } from '@/application/workflowActionTriggerSettingsType';
|
||||
import { type HTTPMethod } from '@/types';
|
||||
import { type InputJsonSchema } from '@/logic-function/input-json-schema.type';
|
||||
|
||||
export type LogicFunctionManifest = SyncableEntityOptions & {
|
||||
name?: string;
|
||||
@@ -9,12 +10,12 @@ export type LogicFunctionManifest = SyncableEntityOptions & {
|
||||
cronTriggerSettings?: CronTriggerSettings;
|
||||
databaseEventTriggerSettings?: DatabaseEventTriggerSettings;
|
||||
httpRouteTriggerSettings?: HttpRouteTriggerSettings;
|
||||
toolTriggerSettings?: ToolTriggerSettings;
|
||||
workflowActionTriggerSettings?: WorkflowActionTriggerSettings;
|
||||
sourceHandlerPath: string;
|
||||
builtHandlerPath: string;
|
||||
builtHandlerChecksum: string;
|
||||
handlerName: string;
|
||||
toolInputSchema: InputJsonSchema;
|
||||
isTool?: boolean;
|
||||
};
|
||||
|
||||
export type CronTriggerSettings = {
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { type InputJsonSchema } from '@/logic-function/input-json-schema.type';
|
||||
|
||||
// Exposes a logic function as an AI tool (chat / MCP / function calling).
|
||||
// Uses standard JSON Schema -- the format LLMs natively understand.
|
||||
// inputSchema is optional in the developer-facing SDK; the manifest
|
||||
// builder fills it in by inferring from the handler source code when omitted.
|
||||
export type ToolTriggerSettings = {
|
||||
inputSchema?: InputJsonSchema;
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
import { type InputSchema } from '@/workflow/types/InputSchema';
|
||||
|
||||
// Exposes a logic function as a step in the visual workflow builder.
|
||||
// Uses Twenty's rich InputSchema with FieldMetadataType support so the
|
||||
// builder can render proper field editors, variable pickers, and labels.
|
||||
// inputSchema is optional in the developer-facing SDK; the manifest builder
|
||||
// fills it in by inferring from the handler source code when omitted.
|
||||
export type WorkflowActionTriggerSettings = {
|
||||
inputSchema?: InputSchema;
|
||||
outputSchema?: InputSchema;
|
||||
icon?: string;
|
||||
label?: string;
|
||||
};
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
import { jsonSchemaToInputSchema } from '@/logic-function/json-schema-to-input-schema';
|
||||
|
||||
describe('jsonSchemaToInputSchema', () => {
|
||||
it('wraps a JSON Schema object into a single-element InputSchema array', () => {
|
||||
const result = jsonSchemaToInputSchema({
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string', description: 'A name' },
|
||||
age: { type: 'number' },
|
||||
},
|
||||
required: ['name'],
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string' },
|
||||
age: { type: 'number' },
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('maps integer to number', () => {
|
||||
const result = jsonSchemaToInputSchema({
|
||||
type: 'object',
|
||||
properties: { count: { type: 'integer' } },
|
||||
});
|
||||
|
||||
expect(result[0].properties).toEqual({ count: { type: 'number' } });
|
||||
});
|
||||
|
||||
it('maps null to unknown', () => {
|
||||
const result = jsonSchemaToInputSchema({
|
||||
type: 'object',
|
||||
properties: { value: { type: 'null' } },
|
||||
});
|
||||
|
||||
expect(result[0].properties).toEqual({ value: { type: 'unknown' } });
|
||||
});
|
||||
|
||||
it('preserves array items', () => {
|
||||
const result = jsonSchemaToInputSchema({
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('preserves enum on string properties', () => {
|
||||
const result = jsonSchemaToInputSchema({
|
||||
type: 'object',
|
||||
properties: {
|
||||
color: { type: 'string', enum: ['red', 'green', 'blue'] },
|
||||
},
|
||||
});
|
||||
|
||||
expect(result[0].properties?.color).toEqual({
|
||||
type: 'string',
|
||||
enum: ['red', 'green', 'blue'],
|
||||
});
|
||||
});
|
||||
|
||||
it('drops non-string enum values silently', () => {
|
||||
const result = jsonSchemaToInputSchema({
|
||||
type: 'object',
|
||||
properties: {
|
||||
mixed: { type: 'string', enum: ['a', 1, true, 'b'] },
|
||||
},
|
||||
});
|
||||
|
||||
expect(result[0].properties?.mixed).toEqual({
|
||||
type: 'string',
|
||||
enum: ['a', 'b'],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,9 +0,0 @@
|
||||
import { type InputJsonSchema } from '@/logic-function';
|
||||
|
||||
export const SEED_LOGIC_FUNCTION_INPUT_SCHEMA: InputJsonSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
a: { type: 'string' },
|
||||
b: { type: 'number' },
|
||||
},
|
||||
};
|
||||
@@ -8,7 +8,7 @@
|
||||
*/
|
||||
|
||||
export { DEFAULT_TOOL_INPUT_SCHEMA } from './constants/DefaultToolInputSchema';
|
||||
export { SEED_LOGIC_FUNCTION_INPUT_SCHEMA } from './constants/SeedLogicFunctionInputSchema';
|
||||
export { getInputSchemaFromSourceCode } from './get-input-schema-from-source-code';
|
||||
export { getOutputSchemaFromValue } from './get-output-schema-from-value';
|
||||
export type { InputJsonSchema } from './input-json-schema.type';
|
||||
export { jsonSchemaToInputSchema } from './json-schema-to-input-schema';
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { type InputJsonSchema } from '@/logic-function/input-json-schema.type';
|
||||
import {
|
||||
type InputSchema,
|
||||
type InputSchemaProperty,
|
||||
} from '@/workflow/types/InputSchema';
|
||||
|
||||
const convertProperty = (jsonSchema: InputJsonSchema): InputSchemaProperty => {
|
||||
const property: InputSchemaProperty = { type: 'unknown' };
|
||||
|
||||
switch (jsonSchema.type) {
|
||||
case 'string':
|
||||
property.type = 'string';
|
||||
break;
|
||||
case 'number':
|
||||
case 'integer':
|
||||
property.type = 'number';
|
||||
break;
|
||||
case 'boolean':
|
||||
property.type = 'boolean';
|
||||
break;
|
||||
case 'array':
|
||||
property.type = 'array';
|
||||
if (jsonSchema.items) {
|
||||
property.items = convertProperty(jsonSchema.items);
|
||||
}
|
||||
break;
|
||||
case 'object':
|
||||
property.type = 'object';
|
||||
if (jsonSchema.properties) {
|
||||
property.properties = Object.fromEntries(
|
||||
Object.entries(jsonSchema.properties).map(([key, value]) => [
|
||||
key,
|
||||
convertProperty(value),
|
||||
]),
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'null':
|
||||
default:
|
||||
property.type = 'unknown';
|
||||
}
|
||||
|
||||
if (Array.isArray(jsonSchema.enum)) {
|
||||
property.enum = jsonSchema.enum.filter(
|
||||
(value): value is string => typeof value === 'string',
|
||||
);
|
||||
}
|
||||
|
||||
return property;
|
||||
};
|
||||
|
||||
// Wraps in a single-element array because Twenty's InputSchema represents
|
||||
// the parameter list of a function -- logic functions take a single params
|
||||
// object, hence a one-element array containing it.
|
||||
export const jsonSchemaToInputSchema = (
|
||||
jsonSchema: InputJsonSchema,
|
||||
): InputSchema => {
|
||||
return [convertProperty(jsonSchema)];
|
||||
};
|
||||
Reference in New Issue
Block a user