From a276f3277f002db029bda763f87efd27fbd31249 Mon Sep 17 00:00:00 2001
From: Etienne <45695613+etiennejouan@users.noreply.github.com>
Date: Wed, 29 Jul 2026 18:48:24 +0200
Subject: [PATCH] feat(workflow): pin concrete model on AI agent node creation
and exclude interactive tools from workflow runs (#23447)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Context
The AI Agent workflow node's model dropdown could show a model that was
not the one used at run time (e.g. the node displayed "Claude Haiku 4.5"
while the run log showed `openai/gpt-5.6-sol`).
Root cause: workflow agents were created with `modelId:
AUTO_SELECT_SMART_MODEL_ID`. The builder's model `Select` cannot
represent that value — auto-select ids are filtered out of the options
(`useWorkspaceAiModelAvailability`) and the pinned "default" option
remaps its value to the resolved concrete model id (`useAiModelOptions`)
— so `Select` silently fell back to `options[0]`, the alphabetically
first enabled model. Meanwhile the runtime correctly resolved
auto-select to the instance's default smart model.
## What this PR does
### 1. New workflow agents store a concrete model id
`WorkflowVersionStepOperationsWorkspaceService` now reads the
workspace's `fastModel` setting, expands it through
`AiModelRegistryService.getEffectiveModelConfig`, validates it with
`validateModelAvailability`, and stores the concrete model id — so the
dropdown displays the model that will actually run, and workflow agents
default to the cheaper fast tier instead of the smart one.
Falls back to `AUTO_SELECT_FAST_MODEL_ID` if the lookup or validation
fails (workspace missing, no AI provider configured, model disabled), so
node creation never breaks.
### 2. Exclude `search_help_center` and `navigate_app` from workflow
agent runs
`ActionToolProvider` adds both tools unconditionally, but they only make
sense in an interactive chat session (navigation targets the user's
browser; help-center search is a support tool). They are now excluded
via `WORKFLOW_AGENT_EXCLUDED_TOOL_NAMES` in `AgentAsyncExecutorService`,
alongside the existing output-navigation exclusions. Chat agents are
unaffected.
## Test coverage
- Existing specs for `WorkflowVersionStepOperationsWorkspaceService` and
`AgentAsyncExecutorService` updated/passing (new constructor deps
mocked).
- `nx typecheck twenty-server` and `lint:diff-with-main` pass.
---
...orkflow-agent-excluded-tool-names.const.ts | 6 ++++
...-step-operations.workspace-service.spec.ts | 21 ++++++++++++
...rsion-step-operations.workspace-service.ts | 32 ++++++++++++++++---
.../workflow-version-step.module.ts | 7 +++-
4 files changed, 61 insertions(+), 5 deletions(-)
create mode 100644 packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/constants/workflow-agent-excluded-tool-names.const.ts
diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/constants/workflow-agent-excluded-tool-names.const.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/constants/workflow-agent-excluded-tool-names.const.ts
new file mode 100644
index 0000000000..db8dc6154c
--- /dev/null
+++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/constants/workflow-agent-excluded-tool-names.const.ts
@@ -0,0 +1,6 @@
+import { type ActionToolId } from 'src/engine/core-modules/tool-provider/constants/action-tool-label.constant';
+
+export const WORKFLOW_AGENT_EXCLUDED_TOOL_NAMES = [
+ 'search_help_center',
+ 'navigate_app',
+] as const satisfies readonly ActionToolId[];
diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/__tests__/workflow-version-step-operations.workspace-service.spec.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/__tests__/workflow-version-step-operations.workspace-service.spec.ts
index ecd3940a6e..e49ad60f00 100644
--- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/__tests__/workflow-version-step-operations.workspace-service.spec.ts
+++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/__tests__/workflow-version-step-operations.workspace-service.spec.ts
@@ -6,7 +6,9 @@ import { SEED_WORKFLOW_ACTION_TRIGGER_SETTINGS } from 'twenty-shared/logic-funct
import { AiAgentRoleService } from 'src/engine/metadata-modules/ai/ai-agent-role/ai-agent-role.service';
import { AgentService } from 'src/engine/metadata-modules/ai/ai-agent/agent.service';
+import { AiModelRegistryService } from 'src/engine/metadata-modules/ai/ai-models/services/ai-model-registry.service';
import { WorkflowVersionCoreSyncService } from 'src/engine/core-modules/workflow/services/workflow-version-core-sync.service';
+import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { createEmptyAllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-all-flat-entity-maps.constant';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { LogicFunctionRuntime } from 'src/engine/metadata-modules/logic-function/logic-function.entity';
@@ -34,6 +36,8 @@ describe('WorkflowVersionStepOperationsWorkspaceService', () => {
let workflowCommonWorkspaceService: jest.Mocked;
let aiAgentRoleService: jest.Mocked;
let workspaceCacheService: jest.Mocked;
+ let aiModelRegistryService: jest.Mocked;
+ let workspaceRepository: jest.Mocked;
beforeEach(async () => {
codeStepBuildService = {
@@ -123,6 +127,15 @@ describe('WorkflowVersionStepOperationsWorkspaceService', () => {
flush: jest.fn(),
} as unknown as jest.Mocked;
+ aiModelRegistryService = {
+ getEffectiveModelConfig: jest.fn(),
+ validateModelAvailability: jest.fn(),
+ } as unknown as jest.Mocked;
+
+ workspaceRepository = {
+ findOneBy: jest.fn().mockResolvedValue(null),
+ };
+
const module: TestingModule = await Test.createTestingModule({
providers: [
WorkflowVersionStepOperationsWorkspaceService,
@@ -150,6 +163,14 @@ describe('WorkflowVersionStepOperationsWorkspaceService', () => {
provide: getRepositoryToken(ObjectMetadataEntity),
useValue: objectMetadataRepository,
},
+ {
+ provide: getRepositoryToken(WorkspaceEntity),
+ useValue: workspaceRepository,
+ },
+ {
+ provide: AiModelRegistryService,
+ useValue: aiModelRegistryService,
+ },
{
provide: WorkflowCommonWorkspaceService,
useValue: workflowCommonWorkspaceService,
diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step-operations.workspace-service.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step-operations.workspace-service.ts
index ceec1313b7..96dec4bb57 100644
--- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step-operations.workspace-service.ts
+++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step-operations.workspace-service.ts
@@ -10,9 +10,9 @@ import {
import { isDefined, isValidUuid } from 'twenty-shared/utils';
import {
IF_ELSE_BRANCH_POSITION_OFFSETS,
+ WorkflowActionType,
getFunctionInputFromInputSchema,
type StepIfElseBranch,
- WorkflowActionType,
} from 'twenty-shared/workflow';
import { Repository } from 'typeorm';
import { v4 } from 'uuid';
@@ -20,14 +20,16 @@ import { v4 } from 'uuid';
import { getFlatFieldsFromFlatObjectMetadata } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util';
import { type WorkflowStepPositionInput } from 'src/engine/core-modules/workflow/dtos/update-workflow-step-position.input';
import { WorkflowVersionCoreSyncService } from 'src/engine/core-modules/workflow/services/workflow-version-core-sync.service';
+import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { AiAgentRoleService } from 'src/engine/metadata-modules/ai/ai-agent-role/ai-agent-role.service';
import { AgentService } from 'src/engine/metadata-modules/ai/ai-agent/agent.service';
+import { AiModelRegistryService } from 'src/engine/metadata-modules/ai/ai-models/services/ai-model-registry.service';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
-import { LogicFunctionFromSourceService } from 'src/engine/metadata-modules/logic-function/services/logic-function-from-source.service';
import {
LogicFunctionException,
LogicFunctionExceptionCode,
} from 'src/engine/metadata-modules/logic-function/logic-function.exception';
+import { LogicFunctionFromSourceService } from 'src/engine/metadata-modules/logic-function/services/logic-function-from-source.service';
import { findFlatLogicFunctionOrThrow } from 'src/engine/metadata-modules/logic-function/utils/find-flat-logic-function-or-throw.util';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity';
@@ -50,7 +52,6 @@ import {
type WorkflowEmptyAction,
type WorkflowFormAction,
} from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
-import { AUTO_SELECT_SMART_MODEL_ID } from 'twenty-shared/constants';
const BASE_STEP_DEFINITION: BaseWorkflowActionSettings = {
outputSchema: {},
errorHandlingOptions: {
@@ -77,10 +78,13 @@ export class WorkflowVersionStepOperationsWorkspaceService {
private readonly logicFunctionFromSourceService: LogicFunctionFromSourceService,
private readonly codeStepBuildService: CodeStepBuildService,
private readonly agentService: AgentService,
+ private readonly aiModelRegistryService: AiModelRegistryService,
@InjectWorkspaceScopedRepository(RoleTargetEntity)
private readonly roleTargetRepository: WorkspaceScopedRepository,
@InjectRepository(ObjectMetadataEntity)
private readonly objectMetadataRepository: Repository,
+ @InjectRepository(WorkspaceEntity)
+ private readonly workspaceRepository: Repository,
private readonly workflowCommonWorkspaceService: WorkflowCommonWorkspaceService,
private readonly aiAgentRoleService: AiAgentRoleService,
private readonly workspaceCacheService: WorkspaceCacheService,
@@ -88,6 +92,26 @@ export class WorkflowVersionStepOperationsWorkspaceService {
private readonly workflowVersionCoreSyncService: WorkflowVersionCoreSyncService,
) {}
+ private async getWorkspaceDefaultFastModelId(
+ workspaceId: string,
+ ): Promise {
+ const workspace = await this.workspaceRepository.findOneBy({
+ id: workspaceId,
+ });
+
+ if (!isDefined(workspace)) {
+ throw new WorkflowVersionStepException(
+ `Workspace ${workspaceId} not found`,
+ WorkflowVersionStepExceptionCode.NOT_FOUND,
+ );
+ }
+
+ const effectiveModelConfig =
+ this.aiModelRegistryService.getEffectiveModelConfig(workspace.fastModel);
+
+ return effectiveModelConfig.modelId;
+ }
+
async runWorkflowVersionStepDeletionSideEffects({
step,
workspaceId,
@@ -547,7 +571,7 @@ export class WorkflowVersionStepOperationsWorkspaceService {
description: '',
prompt:
'You are a helpful AI assistant. Complete the task based on the workflow context.',
- modelId: AUTO_SELECT_SMART_MODEL_ID,
+ modelId: await this.getWorkspaceDefaultFastModelId(workspaceId),
responseFormat: { type: 'text' },
isCustom: true,
},
diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step.module.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step.module.ts
index ccf425079b..a3115d69d3 100644
--- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step.module.ts
+++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step.module.ts
@@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { WorkflowVersionCoreModule } from 'src/engine/core-modules/workflow/workflow-version-core.module';
+import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { AiAgentRoleModule } from 'src/engine/metadata-modules/ai/ai-agent-role/ai-agent-role.module';
import { AiAgentModule } from 'src/engine/metadata-modules/ai/ai-agent/ai-agent.module';
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
@@ -29,7 +30,11 @@ import { WorkflowVersionStepWorkspaceService } from 'src/modules/workflow/workfl
AiAgentRoleModule,
AiAgentModule,
WorkspaceCacheModule,
- TypeOrmModule.forFeature([ObjectMetadataEntity, RoleTargetEntity]),
+ TypeOrmModule.forFeature([
+ ObjectMetadataEntity,
+ RoleTargetEntity,
+ WorkspaceEntity,
+ ]),
WorkspaceManyOrAllFlatEntityMapsCacheModule,
WorkflowVersionCoreModule,
],