feat(workflow): pin concrete model on AI agent node creation and exclude interactive tools from workflow runs (#23447)

## 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.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23447?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
This commit is contained in:
Etienne
2026-07-29 18:48:24 +02:00
committed by GitHub
parent c4a79c50c3
commit a276f3277f
4 changed files with 61 additions and 5 deletions
@@ -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[];
@@ -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<WorkflowCommonWorkspaceService>;
let aiAgentRoleService: jest.Mocked<AiAgentRoleService>;
let workspaceCacheService: jest.Mocked<WorkspaceCacheService>;
let aiModelRegistryService: jest.Mocked<AiModelRegistryService>;
let workspaceRepository: jest.Mocked<any>;
beforeEach(async () => {
codeStepBuildService = {
@@ -123,6 +127,15 @@ describe('WorkflowVersionStepOperationsWorkspaceService', () => {
flush: jest.fn(),
} as unknown as jest.Mocked<WorkspaceCacheService>;
aiModelRegistryService = {
getEffectiveModelConfig: jest.fn(),
validateModelAvailability: jest.fn(),
} as unknown as jest.Mocked<AiModelRegistryService>;
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,
@@ -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<RoleTargetEntity>,
@InjectRepository(ObjectMetadataEntity)
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
@InjectRepository(WorkspaceEntity)
private readonly workspaceRepository: Repository<WorkspaceEntity>,
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<string> {
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,
},
@@ -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,
],