Files
twenty/packages/twenty-server/src/database/commands/upgrade-version-command/2-3/2-3-instance-command-slow-1797000002000-migrate-tool-trigger-settings.ts
T
Félix Malfait 53fdac1417 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>
2026-05-05 14:56:09 +02:00

59 lines
2.3 KiB
TypeScript

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`,
);
}
}