feat(mcp) - optimize instruction prompt and hide get_tool_catalog (#21183)

Workspace-aware initialize.instructions

- Deleted the static mcp-server-instructions.const.ts
- Created build-mcp-server-instructions.util.ts — a comprehensive system
prompt with identity, object list, tool grammar, routing decision tree,
intent mapping, skills vs tools, safety constraints, and data efficiency
guidelines
- Created McpInstructionBuilderService — fetches workspace-specific
object names + skill names and injects them into the instructions


Hide/deprecate get_tool_catalog

Benefit : skip first MCP call (tools are included in instruction)
This commit is contained in:
Etienne
2026-06-03 18:58:32 +02:00
committed by GitHub
parent 4b15b949f3
commit dd0039ca1c
20 changed files with 313 additions and 96 deletions
@@ -1,2 +0,0 @@
export const MCP_SERVER_INSTRUCTIONS =
'Twenty CRM MCP Server. Follow this workflow: (1) get_tool_catalog to discover tools, (2) learn_tools to get input schemas, (3) execute_tool to run them. Never guess tool names — always start with get_tool_catalog. Use load_skills for guidance on complex tasks like workflow or dashboard building. For comparative/grouped analytics (by/per/top/most/least/average/total/ranking), use group_by tools, and use find tools for record retrieval; for multiple metrics, run multiple group_by calls and merge results.';
@@ -4,7 +4,6 @@ import { DEFAULT_TOOL_INPUT_SCHEMA } from 'twenty-shared/logic-function';
import { MCP_PROTOCOL_VERSION } from 'src/engine/api/mcp/constants/mcp-protocol-version.const';
import { MCP_SERVER_INFO } from 'src/engine/api/mcp/constants/mcp-server-info.const';
import { MCP_SERVER_INSTRUCTIONS } from 'src/engine/api/mcp/constants/mcp-server-instructions.const';
import { McpCoreController } from 'src/engine/api/mcp/controllers/mcp-core.controller';
import { type JsonRpc } from 'src/engine/api/mcp/dtos/json-rpc';
import { McpAuthGuard } from 'src/engine/api/mcp/guards/mcp-auth.guard';
@@ -146,7 +145,7 @@ describe('McpCoreController', () => {
prompts: { listChanged: false },
},
serverInfo: MCP_SERVER_INFO,
instructions: MCP_SERVER_INSTRUCTIONS,
instructions: 'mock instructions',
},
};
@@ -245,7 +244,7 @@ describe('McpCoreController', () => {
const mockRequest: JsonRpc = {
jsonrpc: '2.0',
method: 'tools/call',
params: { name: 'get_tool_catalog', arguments: {} },
params: { name: 'execute_tool', arguments: {} },
id: '456',
};
@@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
import { McpCoreController } from 'src/engine/api/mcp/controllers/mcp-core.controller';
import { McpAuthGuard } from 'src/engine/api/mcp/guards/mcp-auth.guard';
import { McpInstructionBuilderService } from 'src/engine/api/mcp/services/mcp-instruction-builder.service';
import { McpProtocolService } from 'src/engine/api/mcp/services/mcp-protocol.service';
import { McpToolExecutorService } from 'src/engine/api/mcp/services/mcp-tool-executor.service';
import { ApiKeyModule } from 'src/engine/core-modules/api-key/api-key.module';
@@ -11,6 +12,7 @@ import { TwentyConfigModule } from 'src/engine/core-modules/twenty-config/twenty
import { ToolProviderModule } from 'src/engine/core-modules/tool-provider/tool-provider.module';
import { JwtAuthGuard } from 'src/engine/guards/jwt-auth.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
import { SkillModule } from 'src/engine/metadata-modules/skill/skill.module';
import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module';
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
@@ -21,6 +23,7 @@ import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/
MetricsModule,
TokenModule,
WorkspaceCacheStorageModule,
WorkspaceManyOrAllFlatEntityMapsCacheModule,
UserRoleModule,
ToolProviderModule,
SkillModule,
@@ -32,6 +35,7 @@ import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/
JwtAuthGuard,
McpAuthGuard,
WorkspaceAuthGuard,
McpInstructionBuilderService,
McpProtocolService,
McpToolExecutorService,
],
@@ -7,19 +7,21 @@ import { MCP_EXECUTE_TOOL_ANNOTATIONS } from 'src/engine/api/mcp/constants/mcp-e
import { MCP_OPEN_WORLD_READ_ONLY_TOOL_ANNOTATIONS } from 'src/engine/api/mcp/constants/mcp-open-world-read-only-tool-annotations.const';
import { MCP_PROTOCOL_VERSION } from 'src/engine/api/mcp/constants/mcp-protocol-version.const';
import { MCP_SERVER_INFO } from 'src/engine/api/mcp/constants/mcp-server-info.const';
import { MCP_SERVER_INSTRUCTIONS } from 'src/engine/api/mcp/constants/mcp-server-instructions.const';
import { type JsonRpc } from 'src/engine/api/mcp/dtos/json-rpc';
import { McpInstructionBuilderService } from 'src/engine/api/mcp/services/mcp-instruction-builder.service';
import { McpProtocolService } from 'src/engine/api/mcp/services/mcp-protocol.service';
import { McpToolExecutorService } from 'src/engine/api/mcp/services/mcp-tool-executor.service';
import { LIST_OBJECT_METADATA_NAMES_TOOL_NAME } from 'src/engine/api/mcp/tools/list-object-metadata-names.tool';
import { LIST_SKILLS_TOOL_NAME } from 'src/engine/api/mcp/tools/list-skills.tool';
import { type McpToolAnnotations } from 'src/engine/api/mcp/types/mcp-tool-annotations.type';
import { type FlatApiKey } from 'src/engine/core-modules/api-key/types/flat-api-key.type';
import { ApiKeyRoleService } from 'src/engine/core-modules/api-key/services/api-key-role.service';
import { EXECUTE_TOOL_TOOL_NAME } from 'src/engine/core-modules/tool-provider/tools/execute-tool.tool';
import { GET_TOOL_CATALOG_TOOL_NAME } from 'src/engine/core-modules/tool-provider/tools/get-tool-catalog.tool';
import { LEARN_TOOLS_TOOL_NAME } from 'src/engine/core-modules/tool-provider/tools/learn-tools.tool';
import { LOAD_SKILL_TOOL_NAME } from 'src/engine/core-modules/tool-provider/tools/load-skill.tool';
import { ToolRegistryService } from 'src/engine/core-modules/tool-provider/services/tool-registry.service';
import { type FlatWorkspace } from 'src/engine/core-modules/workspace/types/flat-workspace.type';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { SkillService } from 'src/engine/metadata-modules/skill/skill.service';
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
@@ -40,10 +42,11 @@ describe('McpProtocolService', () => {
} as FlatApiKey;
const EXPECTED_MCP_TOOL_NAMES = [
GET_TOOL_CATALOG_TOOL_NAME,
LEARN_TOOLS_TOOL_NAME,
EXECUTE_TOOL_TOOL_NAME,
LOAD_SKILL_TOOL_NAME,
LIST_OBJECT_METADATA_NAMES_TOOL_NAME,
LIST_SKILLS_TOOL_NAME,
'search_help_center',
] as const;
@@ -51,10 +54,12 @@ describe('McpProtocolService', () => {
(typeof EXPECTED_MCP_TOOL_NAMES)[number],
McpToolAnnotations
> = {
[GET_TOOL_CATALOG_TOOL_NAME]: MCP_CLOSED_WORLD_READ_ONLY_TOOL_ANNOTATIONS,
[LEARN_TOOLS_TOOL_NAME]: MCP_CLOSED_WORLD_READ_ONLY_TOOL_ANNOTATIONS,
[EXECUTE_TOOL_TOOL_NAME]: MCP_EXECUTE_TOOL_ANNOTATIONS,
[LOAD_SKILL_TOOL_NAME]: MCP_CLOSED_WORLD_READ_ONLY_TOOL_ANNOTATIONS,
[LIST_OBJECT_METADATA_NAMES_TOOL_NAME]:
MCP_CLOSED_WORLD_READ_ONLY_TOOL_ANNOTATIONS,
[LIST_SKILLS_TOOL_NAME]: MCP_CLOSED_WORLD_READ_ONLY_TOOL_ANNOTATIONS,
search_help_center: MCP_OPEN_WORLD_READ_ONLY_TOOL_ANNOTATIONS,
};
@@ -103,6 +108,20 @@ describe('McpProtocolService', () => {
findAllFlatSkills: jest.fn().mockResolvedValue([]),
},
},
{
provide: McpInstructionBuilderService,
useValue: {
buildInstructions: jest.fn().mockResolvedValue('mock instructions'),
},
},
{
provide: WorkspaceManyOrAllFlatEntityMapsCacheService,
useValue: {
getOrRecomputeManyOrAllFlatEntityMaps: jest.fn().mockResolvedValue({
flatObjectMetadataMaps: { byUniversalIdentifier: {} },
}),
},
},
],
}).compile();
@@ -118,9 +137,12 @@ describe('McpProtocolService', () => {
});
describe('handleInitialize', () => {
it('should return spec-compliant initialization response', () => {
it('should return spec-compliant initialization response', async () => {
const requestId = '123';
const result = service.handleInitialize(requestId);
const result = await service.handleInitialize(
requestId,
mockWorkspace.id,
);
expect(result).toEqual({
id: requestId,
@@ -133,7 +155,7 @@ describe('McpProtocolService', () => {
prompts: { listChanged: false },
},
serverInfo: MCP_SERVER_INFO,
instructions: MCP_SERVER_INSTRUCTIONS,
instructions: expect.any(String),
},
});
});
@@ -210,7 +232,7 @@ describe('McpProtocolService', () => {
prompts: { listChanged: false },
},
serverInfo: MCP_SERVER_INFO,
instructions: MCP_SERVER_INSTRUCTIONS,
instructions: expect.any(String),
},
});
});
@@ -230,7 +252,7 @@ describe('McpProtocolService', () => {
expect(result).toBeNull();
});
it('should build a ToolSet with exactly 5 tools and pass it to executor for tools/call', async () => {
it('should build a ToolSet with exactly 6 tools and pass it to executor for tools/call', async () => {
userRoleService.getRoleIdForUserWorkspace.mockResolvedValue(mockRoleId);
const mockToolCallResponse = {
@@ -281,7 +303,7 @@ describe('McpProtocolService', () => {
);
});
it('should build a ToolSet with exactly 5 tools and pass it to executor for tools/list', async () => {
it('should build a ToolSet with exactly 6 tools and pass it to executor for tools/list', async () => {
userRoleService.getRoleIdForUserWorkspace.mockResolvedValue(mockRoleId);
mcpToolExecutorService.handleToolsListing.mockReturnValue({
@@ -397,7 +419,7 @@ describe('McpProtocolService', () => {
const mockRequest: JsonRpc = {
jsonrpc: '2.0',
method: 'tools/call',
params: { name: 'get_tool_catalog', arguments: {} },
params: { name: 'execute_tool', arguments: {} },
id: '123',
};
@@ -0,0 +1,40 @@
import { Injectable } from '@nestjs/common';
import { camelToSnakeCase } from 'twenty-shared/utils';
import { buildMcpServerInstructions } from 'src/engine/api/mcp/utils/build-mcp-server-instructions.util';
import { getDatabaseCrudToolFlatObjects } from 'src/engine/metadata-modules/ai/ai-agent/utils/get-database-crud-tool-flat-objects.util';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { SkillService } from 'src/engine/metadata-modules/skill/skill.service';
@Injectable()
export class McpInstructionBuilderService {
constructor(
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
private readonly skillService: SkillService,
) {}
async buildInstructions(workspaceId: string): Promise<string> {
const [{ flatObjectMetadataMaps }, allSkills] = await Promise.all([
this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps({
workspaceId,
flatMapsKeys: ['flatObjectMetadataMaps'],
}),
this.skillService.findAllFlatSkills(workspaceId),
]);
const objectNames = getDatabaseCrudToolFlatObjects(
flatObjectMetadataMaps.byUniversalIdentifier,
)
.map((obj) => camelToSnakeCase(obj.namePlural))
.sort()
.join(', ');
const skillNames =
allSkills.length > 0
? allSkills.map((skill) => skill.name).join(', ')
: undefined;
return buildMcpServerInstructions(objectNames, skillNames);
}
}
@@ -10,17 +10,32 @@ import { MCP_EXECUTE_TOOL_ANNOTATIONS } from 'src/engine/api/mcp/constants/mcp-e
import { MCP_OPEN_WORLD_READ_ONLY_TOOL_ANNOTATIONS } from 'src/engine/api/mcp/constants/mcp-open-world-read-only-tool-annotations.const';
import { MCP_PROTOCOL_VERSION } from 'src/engine/api/mcp/constants/mcp-protocol-version.const';
import { MCP_SERVER_INFO } from 'src/engine/api/mcp/constants/mcp-server-info.const';
import { MCP_SERVER_INSTRUCTIONS } from 'src/engine/api/mcp/constants/mcp-server-instructions.const';
import { type JsonRpc } from 'src/engine/api/mcp/dtos/json-rpc';
import { McpInstructionBuilderService } from 'src/engine/api/mcp/services/mcp-instruction-builder.service';
import { McpToolExecutorService } from 'src/engine/api/mcp/services/mcp-tool-executor.service';
import {
createListObjectMetadataNamesTool,
LIST_OBJECT_METADATA_NAMES_TOOL_NAME,
listObjectMetadataNamesInputSchema,
} from 'src/engine/api/mcp/tools/list-object-metadata-names.tool';
import {
createListSkillsTool,
LIST_SKILLS_TOOL_NAME,
listSkillsInputSchema,
} from 'src/engine/api/mcp/tools/list-skills.tool';
import { type McpToolAnnotations } from 'src/engine/api/mcp/types/mcp-tool-annotations.type';
import { wrapJsonRpcResponse } from 'src/engine/api/mcp/utils/wrap-jsonrpc-response.util';
import { type FlatApiKey } from 'src/engine/core-modules/api-key/types/flat-api-key.type';
import { ApiKeyRoleService } from 'src/engine/core-modules/api-key/services/api-key-role.service';
import { type FlatApiKey } from 'src/engine/core-modules/api-key/types/flat-api-key.type';
import { type WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
import { buildApiKeyAuthContext } from 'src/engine/core-modules/auth/utils/build-api-key-auth-context.util';
import { COMMON_PRELOAD_TOOLS } from 'src/engine/core-modules/tool-provider/constants/common-preload-tools.const';
import { ToolRegistryService } from 'src/engine/core-modules/tool-provider/services/tool-registry.service';
import {
createLearnToolsTool,
LEARN_TOOLS_TOOL_NAME,
learnToolsInputSchema,
} from 'src/engine/core-modules/tool-provider/tools';
import {
createExecuteToolTool,
EXECUTE_TOOL_TOOL_NAME,
@@ -31,17 +46,13 @@ import {
GET_TOOL_CATALOG_TOOL_NAME,
getToolCatalogInputSchema,
} from 'src/engine/core-modules/tool-provider/tools/get-tool-catalog.tool';
import {
createLearnToolsTool,
LEARN_TOOLS_TOOL_NAME,
learnToolsInputSchema,
} from 'src/engine/core-modules/tool-provider/tools/learn-tools.tool';
import {
createLoadSkillTool,
LOAD_SKILL_TOOL_NAME,
loadSkillInputSchema,
} from 'src/engine/core-modules/tool-provider/tools/load-skill.tool';
import { type FlatWorkspace } from 'src/engine/core-modules/workspace/types/flat-workspace.type';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { SkillService } from 'src/engine/metadata-modules/skill/skill.service';
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
@@ -80,9 +91,14 @@ export class McpProtocolService {
private readonly mcpToolExecutorService: McpToolExecutorService,
private readonly apiKeyRoleService: ApiKeyRoleService,
private readonly skillService: SkillService,
private readonly mcpInstructionBuilderService: McpInstructionBuilderService,
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
) {}
handleInitialize(requestId: string | number) {
async handleInitialize(requestId: string | number, workspaceId: string) {
const instructions =
await this.mcpInstructionBuilderService.buildInstructions(workspaceId);
return wrapJsonRpcResponse(requestId, {
result: {
protocolVersion: MCP_PROTOCOL_VERSION,
@@ -92,7 +108,7 @@ export class McpProtocolService {
prompts: { listChanged: false },
},
serverInfo: MCP_SERVER_INFO,
instructions: MCP_SERVER_INSTRUCTIONS,
instructions,
},
});
}
@@ -162,15 +178,6 @@ export class McpProtocolService {
inputSchema: zodSchema(getToolCatalogInputSchema),
annotations: MCP_CLOSED_WORLD_READ_ONLY_TOOL_ANNOTATIONS,
} as McpAnnotatedTool,
[LEARN_TOOLS_TOOL_NAME]: {
...createLearnToolsTool(
this.toolRegistry,
toolContext,
MCP_EXCLUDED_TOOL_NAMES,
),
inputSchema: zodSchema(learnToolsInputSchema),
annotations: MCP_CLOSED_WORLD_READ_ONLY_TOOL_ANNOTATIONS,
} as McpAnnotatedTool,
[EXECUTE_TOOL_TOOL_NAME]: {
...createExecuteToolTool(this.toolRegistry, toolContext, {
excludeTools: MCP_EXCLUDED_TOOL_NAMES,
@@ -193,6 +200,28 @@ export class McpProtocolService {
inputSchema: zodSchema(loadSkillInputSchema),
annotations: MCP_CLOSED_WORLD_READ_ONLY_TOOL_ANNOTATIONS,
} as McpAnnotatedTool,
[LIST_OBJECT_METADATA_NAMES_TOOL_NAME]: {
...createListObjectMetadataNamesTool(
this.flatEntityMapsCacheService,
workspace.id,
),
inputSchema: zodSchema(listObjectMetadataNamesInputSchema),
annotations: MCP_CLOSED_WORLD_READ_ONLY_TOOL_ANNOTATIONS,
} as McpAnnotatedTool,
[LIST_SKILLS_TOOL_NAME]: {
...createListSkillsTool(this.skillService, workspace.id),
inputSchema: zodSchema(listSkillsInputSchema),
annotations: MCP_CLOSED_WORLD_READ_ONLY_TOOL_ANNOTATIONS,
} as McpAnnotatedTool,
[LEARN_TOOLS_TOOL_NAME]: {
...createLearnToolsTool(
this.toolRegistry,
toolContext,
MCP_EXCLUDED_TOOL_NAMES,
),
inputSchema: zodSchema(learnToolsInputSchema),
annotations: MCP_CLOSED_WORLD_READ_ONLY_TOOL_ANNOTATIONS,
} as McpAnnotatedTool,
};
}
@@ -219,7 +248,7 @@ export class McpProtocolService {
}
if (method === 'initialize') {
return this.handleInitialize(id);
return this.handleInitialize(id, workspace.id);
}
if (method === 'ping') {
@@ -0,0 +1,43 @@
import { z } from 'zod';
import { camelToSnakeCase } from 'twenty-shared/utils';
import { getDatabaseCrudToolFlatObjects } from 'src/engine/metadata-modules/ai/ai-agent/utils/get-database-crud-tool-flat-objects.util';
import { type WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
export const LIST_OBJECT_METADATA_NAMES_TOOL_NAME =
'list_object_metadata_names';
export const listObjectMetadataNamesInputSchema = z.object({});
export type ListObjectMetadataNamesResult = {
objectNames: string[];
message: string;
};
export const createListObjectMetadataNamesTool = (
flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
workspaceId: string,
) => ({
description:
'List all available object metadata names in the workspace. Use this to get a fresh list of objects when the initial instructions may be outdated.',
inputSchema: listObjectMetadataNamesInputSchema,
execute: async (): Promise<ListObjectMetadataNamesResult> => {
const { flatObjectMetadataMaps } =
await flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps({
workspaceId,
flatMapsKeys: ['flatObjectMetadataMaps'],
});
const objectNames = getDatabaseCrudToolFlatObjects(
flatObjectMetadataMaps.byUniversalIdentifier,
)
.map((obj) => camelToSnakeCase(obj.namePlural))
.sort();
return {
objectNames,
message: `Found ${objectNames.length} object(s): ${objectNames.join(', ')}.`,
};
},
});
@@ -0,0 +1,34 @@
import { z } from 'zod';
import { type SkillService } from 'src/engine/metadata-modules/skill/skill.service';
export const LIST_SKILLS_TOOL_NAME = 'list_skills';
export const listSkillsInputSchema = z.object({});
export type ListSkillsResult = {
skillNames: string[];
message: string;
};
export const createListSkillsTool = (
skillService: SkillService,
workspaceId: string,
) => ({
description:
'List all available skill names in the workspace. Use this to get a fresh list of skills when the initial instructions may be outdated.',
inputSchema: listSkillsInputSchema,
execute: async (): Promise<ListSkillsResult> => {
const allSkills = await skillService.findAllFlatSkills(workspaceId);
const skillNames = allSkills.map((skill) => skill.name);
return {
skillNames,
message:
skillNames.length > 0
? `Found ${skillNames.length} skill(s): ${skillNames.join(', ')}.`
: 'No skills are currently available in this workspace.',
};
},
});
@@ -0,0 +1,70 @@
export const buildMcpServerInstructions = (
objectNames: string,
skillNames?: string,
): string =>
[
`You are an AI assistant for a Twenty CRM workspace.`,
`Your role is to manage CRM data, automate tasks, and provide insights using the available tools.`,
``,
`Available objects: ${objectNames}.`,
``,
`Key relations:`,
` person.companyId → company`,
` note / task / activity are attachable to any object via targetRecordId`,
``,
`Meta-tools (always available):`,
` execute_tool(toolName, arguments) — execute any CRUD or action tool by name`,
` learn_tools(toolNames) — fetch input schema for specific tools before calling them`,
` load_skills(skillNames) — load step-by-step instructions for complex tasks`,
``,
...(skillNames ? [`Available skills: ${skillNames}.`, ``] : []),
`CRUD tool name grammar — construct names directly without prior discovery:`,
` Read: find_{objects} | find_one_{object} | group_by_{objects}`,
` Write: create_{object} | create_many_{objects} | update_{object} | update_many_{objects} | delete_{object}`,
``,
`Non-CRUD tools — use learn_tools for schemas:`,
` ACTION: http_request | send_email | draft_email | navigate_app | code_interpreter | search_help_center`,
` WORKFLOW: create_complete_workflow | create/update/delete_workflow_version_step | activate/deactivate_workflow_version`,
` METADATA: get/create/update/delete_object_metadata | get/create/update/delete_field_metadata`,
` VIEW: get_views | get_view_query_parameters | create/update/delete_view | manage view fields, filters, sorts`,
` DASHBOARD: list_dashboards | get_dashboard | create_complete_dashboard | add/update/delete_dashboard_widget`,
` WEBHOOK: list/create/update/delete_webhook`,
` NAVIGATION: list/create/update/delete_navigation_menu_item`,
` LOGIC_FUNCTION: app_{function_name} — workspace-specific; use list_logic_function_tools to discover`,
``,
`Skills vs Tools:`,
` Skills = documentation (load_skills) — teach HOW to do something, correct schemas and patterns`,
` Tools = execution (execute_tool) — let you DO something`,
` For complex tasks (workflows, dashboards, metadata), load the matching skill BEFORE calling tools.`,
` ⚠️ Never call workflow, dashboard, or metadata tools without loading their skill first.`,
``,
`Route by intent:`,
` Named entity ("Acme company") → find_{objects} to resolve id first, then operate on id`,
` Retrieve records → find_{objects} (default limit: 10, always report total count)`,
` Single record by id → find_one_{object}`,
` Analytics / grouped metrics → group_by_{objects} (COUNT, SUM, AVG, MIN, MAX)`,
` Multiple metrics → run parallel group_by calls, merge results`,
` Non-CRUD task → use tool name from the list above + learn_tools for schema`,
``,
`Execution rules:`,
` Independent tool calls → run in parallel`,
` Dependent operations → run sequentially (find-then-mutate)`,
``,
`Data efficiency:`,
` Default limit: 10. Only increase if user explicitly needs more.`,
` Always apply filters to narrow results — don't fetch all records of a type.`,
` Use batch tools (create_many_*, update_many_*) instead of looping single-item calls.`,
``,
`Destructive operations (update_many, delete):`,
` 1. Run find_{objects} with the same filter — state record count to user`,
` 2. Wait for explicit confirmation before executing`,
``,
`Twenty primitives:`,
` Favorites are navigation menu items. To favorite something, call create_navigation_menu_item with scope: 'user'.`,
` A default OBJECT navigation item is auto-created with create_object_metadata — do not add another.`,
` http_request is ONLY for external third-party APIs, never for Twenty's own data.`,
``,
`On tool failure: read the error message, do not retry silently, report to user.`,
`Present results as readable summaries, not raw JSON.`,
`For large result sets, show count + first N records and offer to paginate.`,
].join('\n');