feat(ai): replace agent search with skills system (#16513)

## Summary

- Replace the agent search mechanism with a new skills-based system
- Add a `skills` module with predefined skill definitions that the AI
can load on demand
- Remove specialized agents (workflow-builder, data-manipulator,
dashboard-builder, metadata-builder, researcher), keeping only the
helper agent
- Add `recordReferences` to workflow creation tool for chip linking in
the UI

## Changes

### New Skills Module
- `skill-definitions.ts` - Contains 5 skill definitions with detailed
instructions
- `skills.service.ts` - Service to get skills by name
- `load-skill.tool.ts` - Tool for AI to load skills explicitly

### Removed
- `agent-search.tool.ts` - Replaced by skill loading
- Specialized agent definitions (converted to skills)

### Updated
- Chat execution now shows skill catalog in system prompt
- Workflow creation returns `recordReferences` for UI linking

## Test plan
- [ ] Verify AI can load skills using `load_skill` tool
- [ ] Verify skill content is returned correctly
- [ ] Verify workflow creation shows clickable chip in chat
- [ ] Verify helper agent still works
This commit is contained in:
Félix Malfait
2025-12-12 06:51:25 +01:00
committed by GitHub
parent 4d90a8f6e3
commit 70a78aafe9
27 changed files with 488 additions and 485 deletions
@@ -0,0 +1,6 @@
export type SkillDefinition = {
name: string;
label: string;
description: string;
content: string;
};
@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { SkillsService } from './skills.service';
@Module({
providers: [SkillsService],
exports: [SkillsService],
})
export class SkillsModule {}
@@ -0,0 +1,56 @@
import { Injectable } from '@nestjs/common';
import { SkillDefinition } from 'src/engine/core-modules/skills/skill-definition.type';
import { DASHBOARD_BUILDING_SKILL } from 'src/engine/core-modules/skills/skills/dashboard-building.skill';
import { DATA_MANIPULATION_SKILL } from 'src/engine/core-modules/skills/skills/data-manipulation.skill';
import { METADATA_BUILDING_SKILL } from 'src/engine/core-modules/skills/skills/metadata-building.skill';
import { RESEARCH_SKILL } from 'src/engine/core-modules/skills/skills/research.skill';
import { WORKFLOW_BUILDING_SKILL } from 'src/engine/core-modules/skills/skills/workflow-building.skill';
const SKILL_DEFINITIONS: SkillDefinition[] = [
WORKFLOW_BUILDING_SKILL,
DATA_MANIPULATION_SKILL,
DASHBOARD_BUILDING_SKILL,
METADATA_BUILDING_SKILL,
RESEARCH_SKILL,
];
export type Skill = {
name: string;
label: string;
description: string;
content: string;
};
@Injectable()
export class SkillsService {
getAllSkills(): Skill[] {
return SKILL_DEFINITIONS.map((skill) => ({
name: skill.name,
label: skill.label,
description: skill.description,
content: skill.content,
}));
}
getSkillByName(name: string): Skill | undefined {
const skillDef = SKILL_DEFINITIONS.find((skill) => skill.name === name);
if (!skillDef) {
return undefined;
}
return {
name: skillDef.name,
label: skillDef.label,
description: skillDef.description,
content: skillDef.content,
};
}
getSkillsByNames(names: string[]): Skill[] {
return names
.map((name) => this.getSkillByName(name))
.filter((skill): skill is Skill => skill !== undefined);
}
}
@@ -0,0 +1,66 @@
import { type SkillDefinition } from 'src/engine/core-modules/skills/skill-definition.type';
export const DASHBOARD_BUILDING_SKILL: SkillDefinition = {
name: 'dashboard-building',
label: 'Dashboard Building',
description: 'Creating and managing dashboards with widgets and layouts',
content: `# Dashboard Building Skill
You help users create and manage dashboards with widgets.
## Capabilities
- Create new dashboards from scratch
- Add, modify, and remove widgets from dashboards
- Configure widget types (VIEW, GRAPH, FIELDS, TIMELINE, TASKS, NOTES, FILES, EMAILS, CALENDAR, RICH_TEXT, IFRAME, WORKFLOW)
- Manage dashboard tabs and layouts
- Position widgets in a grid system (12-column layout)
## Dashboard Structure
- **Dashboard**: Container with a title and pageLayout
- **PageLayout**: Contains tabs (type: DASHBOARD)
- **PageLayoutTab**: Contains widgets with a title, position, and layoutMode (grid/vertical-list/canvas)
- **PageLayoutWidget**: Individual widget with type, title, gridPosition, and optional configuration
## Grid System
- 12 columns total
- Grid positions: { row, column, rowSpan, columnSpan }
- Common sizes: Full width (columnSpan: 12), Half width (columnSpan: 6), Quarter width (columnSpan: 3)
- Typical heights: Small (rowSpan: 4), Medium (rowSpan: 6), Large (rowSpan: 8)
## Widget Types Explained
- **VIEW**: Display a filtered view of records (companies, people, opportunities, etc.)
- **GRAPH**: Show charts and visualizations of data
- **FIELDS**: Display specific fields from a record
- **TIMELINE**: Show activity timeline
- **TASKS**: Display tasks list
- **NOTES**: Show notes
- **FILES**: Display file attachments
- **EMAILS**: Show email communications
- **CALENDAR**: Display calendar events
- **RICH_TEXT**: Custom text content
- **IFRAME**: Embed external content
- **WORKFLOW**: Display workflow information
## Approach
- Ask clarifying questions about dashboard purpose and desired widgets
- Suggest appropriate widget types and layouts for the use case
- Create well-organized, visually balanced dashboards
- For modifications, first understand current structure
- Explain widget placement and purpose
- Consider responsive design (widgets stack on smaller screens)
## Layout Best Practices
- Place most important information at the top
- Group related widgets together
- Use consistent widget sizes when possible
- Leave some whitespace for visual clarity
- Consider logical reading order (left to right, top to bottom)
Prioritize user needs and dashboard usability.`,
};
@@ -0,0 +1,44 @@
import { type SkillDefinition } from 'src/engine/core-modules/skills/skill-definition.type';
export const DATA_MANIPULATION_SKILL: SkillDefinition = {
name: 'data-manipulation',
label: 'Data Manipulation',
description:
'Searching, filtering, creating, and updating records across all objects',
content: `# Data Manipulation Skill
You explore and manage data across companies, people, opportunities, tasks, notes, and custom objects.
## Capabilities
- Search, filter, sort, create, update records
- Manage relationships between records
- Bulk operations and data analysis
## Constraints
- READ and WRITE access to all objects
- CANNOT delete records or access workflow objects
- CANNOT modify workspace settings
## Multi-step Approach
- Chain queries to solve complex requests (e.g., find companies → get their opportunities → calculate totals)
- If a query fails or returns no results, try alternative filters or approaches
- Validate data exists before referencing it (search before update)
- Use results from one query to inform the next
- Try 2-3 different approaches before giving up
## Sorting (Critical)
For "top N" queries, use orderBy with limit:
- Examples: orderBy: [{"employees": "DescNullsLast"}], orderBy: [{"createdAt": "AscNullsFirst"}]
- Valid directions: "AscNullsFirst", "AscNullsLast", "DescNullsFirst", "DescNullsLast"
## Before Bulk Operations
- Confirm the scope and impact
- Explain what will change
Prioritize data integrity and provide clear feedback on operations performed.`,
};
@@ -0,0 +1,64 @@
import { type SkillDefinition } from 'src/engine/core-modules/skills/skill-definition.type';
export const METADATA_BUILDING_SKILL: SkillDefinition = {
name: 'metadata-building',
label: 'Metadata Building',
description:
'Managing the data model: creating objects, fields, and relations',
content: `# Metadata Building Skill
You help users manage their workspace data model by creating, updating, and organizing custom objects and fields.
## Capabilities
- Create new custom objects with appropriate naming and configuration
- Add fields to existing objects (text, number, date, select, relation, etc.)
- Update object and field properties (labels, descriptions, icons)
- Manage field settings (required, unique, default values)
- Create relations between objects
## Key Concepts
- **Objects**: Represent entities in the data model (e.g., Company, Person, Opportunity)
- **Fields**: Properties of objects with specific types (TEXT, NUMBER, DATE_TIME, SELECT, RELATION, etc.)
- **Relations**: Links between objects (one-to-many, many-to-one)
- **Labels vs Names**: Labels are for display, names are internal identifiers (camelCase)
## Field Types Available
- **TEXT**: Simple text fields
- **NUMBER**: Numeric values (integers or decimals)
- **BOOLEAN**: True/false values
- **DATE_TIME**: Date and time values
- **DATE**: Date only values
- **SELECT**: Single choice from options
- **MULTI_SELECT**: Multiple choices from options
- **LINK**: URL fields
- **LINKS**: Multiple URL fields
- **EMAIL**: Email address fields
- **EMAILS**: Multiple email fields
- **PHONE**: Phone number fields
- **PHONES**: Multiple phone fields
- **CURRENCY**: Monetary values
- **RATING**: Star ratings
- **RELATION**: Links to other objects
- **RICH_TEXT**: Formatted text content
## Best Practices
- Use clear, descriptive names for objects and fields
- Follow naming conventions: singular for object names, camelCase for field names
- Add helpful descriptions to objects and fields
- Choose appropriate field types for the data being stored
- Consider relationships between objects when designing the data model
## Approach
- Ask clarifying questions to understand the user's data modeling needs
- Suggest best practices for naming and organization
- Explain the impact of changes to the data model
- Verify object and field existence before making updates
- Provide clear feedback on operations performed
Prioritize data model integrity and user understanding.`,
};
@@ -0,0 +1,34 @@
import { type SkillDefinition } from 'src/engine/core-modules/skills/skill-definition.type';
export const RESEARCH_SKILL: SkillDefinition = {
name: 'research',
label: 'Research',
description: 'Finding information and gathering facts from the web',
content: `# Research Skill
You find information and gather facts from the web.
## Capabilities
- Search for current information and facts
- Research companies, people, technologies, trends
- Gather competitive intelligence and market data
- Find contact details and verify information
## Research Strategy
- Try multiple search queries from different angles
- If initial searches fail, use alternative search terms
- Cross-reference information when possible
- Cite sources and provide context
## Present Findings
- Be thorough but concise
- Organize information logically
- Distinguish facts from speculation
- Note if information might be outdated
- Include relevant sources
Be persistent in finding accurate information.`,
};
@@ -0,0 +1,62 @@
import { type SkillDefinition } from 'src/engine/core-modules/skills/skill-definition.type';
export const WORKFLOW_BUILDING_SKILL: SkillDefinition = {
name: 'workflow-building',
label: 'Workflow Building',
description:
'Creating and managing automation workflows with triggers and steps',
content: `# Workflow Building Skill
You help users create and manage automation workflows.
## Capabilities
- Create workflows from scratch
- Modify existing workflows (add, remove, update steps)
- Explain workflow structure and suggest improvements
## Key Concepts
- **Triggers**: DATABASE_EVENT, MANUAL, CRON, WEBHOOK
- **Steps**: CREATE_RECORD, SEND_EMAIL, CODE, etc.
- **Data flow**: Use {{stepId.fieldName}} to reference previous step outputs
- **Relationships**: Use nested objects like {"company": {"id": "{{reference}}"}}
## CRON Trigger Settings Schema
For CRON triggers, settings.type must be one of these exact values:
1. **DAYS** - Daily schedule
- Requires: schedule: { day: number (1+), hour: number (0-23), minute: number (0-59) }
- Example: { type: "DAYS", schedule: { day: 1, hour: 9, minute: 0 }, outputSchema: {} }
2. **HOURS** - Hourly schedule (USE THIS FOR "EVERY HOUR")
- Requires: schedule: { hour: number (1+), minute: number (0-59) }
- Example: { type: "HOURS", schedule: { hour: 1, minute: 0 }, outputSchema: {} }
- This runs every X hours at Y minutes past the hour
3. **MINUTES** - Minute-based schedule
- Requires: schedule: { minute: number (1+) }
- Example: { type: "MINUTES", schedule: { minute: 15 }, outputSchema: {} }
4. **CUSTOM** - Custom cron pattern
- Requires: pattern: string (cron expression)
- Example: { type: "CUSTOM", pattern: "0 * * * *", outputSchema: {} }
## Critical Notes
Always rely on tool schema definitions:
- The workflow creation tool provides comprehensive schemas with examples
- Follow schema definitions exactly for field names, types, and structures
- Schema includes validation rules and common patterns
## Approach
- Ask clarifying questions to understand user needs
- Suggest appropriate actions for the use case
- Explain each step and why it's needed
- For modifications, understand current structure first
- Ensure workflow logic remains coherent
Prioritize user understanding and workflow effectiveness.`,
};
@@ -26,8 +26,9 @@ import { ToolRegistryService } from './services/tool-registry.service';
// -> WorkflowRunnerModule -> WorkflowExecutorModule -> AiAgentActionModule
// -> AiAgentExecutionModule -> ToolProviderModule
//
// Instead, WorkflowToolWorkspaceService is an optional dependency that must be
// provided by the importing module (e.g., AiChatModule imports WorkflowToolsModule).
// Instead, WorkflowToolsModule is a @Global() module that provides WORKFLOW_TOOL_SERVICE_TOKEN.
// When WorkflowToolsModule is imported anywhere in the app (e.g., AiChatModule),
// the token becomes available globally to WorkflowToolProvider via @Optional() injection.
@Module({
imports: [
@@ -1,61 +0,0 @@
import { z } from 'zod';
import { type AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
export const AGENT_SEARCH_TOOL_NAME = 'agent_search';
export const agentSearchInputSchema = z.object({
input: z.object({
query: z.string().describe('What kind of expertise or help you need'),
limit: z
.number()
.optional()
.default(2)
.describe('Maximum number of agents to return'),
}),
});
export type AgentSearchInput = z.infer<typeof agentSearchInputSchema>['input'];
export type AgentSearchResult = {
agents: Array<{
name: string;
label: string;
expertise: string;
}>;
message: string;
};
export type AgentSearchFunction = (
query: string,
options: { limit: number },
) => Promise<AgentEntity[]>;
export const createAgentSearchTool = (searchAgents: AgentSearchFunction) => ({
description:
'Search for agent expertise/skills to help with specialized tasks. Returns agent instructions that provide domain knowledge for workflows, data manipulation, metadata management, etc.',
inputSchema: agentSearchInputSchema,
execute: async (parameters: {
input: AgentSearchInput;
}): Promise<AgentSearchResult> => {
const { query, limit = 2 } = parameters.input;
const agents = await searchAgents(query, { limit });
if (agents.length === 0) {
return {
agents: [],
message: `No agent expertise found matching "${query}". Try searching for: "workflow", "data", "metadata", "dashboard", or "research".`,
};
}
return {
agents: agents.map((agent) => ({
name: agent.name,
label: agent.label,
expertise: agent.prompt,
})),
message: `Found ${agents.length} agent(s) with relevant expertise. Their instructions are included above to help guide your approach.`,
};
},
});
@@ -1,17 +1,17 @@
export {
createLoadToolsTool,
LOAD_TOOLS_TOOL_NAME,
createLoadToolsTool,
loadToolsInputSchema,
type DynamicToolStore,
type LoadToolsInput,
type LoadToolsResult,
type DynamicToolStore,
} from './load-tools.tool';
export {
createAgentSearchTool,
AGENT_SEARCH_TOOL_NAME,
agentSearchInputSchema,
type AgentSearchInput,
type AgentSearchResult,
type AgentSearchFunction,
} from './agent-search.tool';
LOAD_SKILL_TOOL_NAME,
createLoadSkillTool,
loadSkillInputSchema,
type LoadSkillFunction,
type LoadSkillInput,
type LoadSkillResult,
} from './load-skill.tool';
@@ -0,0 +1,57 @@
import { z } from 'zod';
import { type Skill } from 'src/engine/core-modules/skills/skills.service';
export const LOAD_SKILL_TOOL_NAME = 'load_skill';
export const loadSkillInputSchema = z.object({
input: z.object({
skillNames: z
.array(z.string())
.describe(
'Names of the skills to load (e.g., ["workflow-building", "data-manipulation"])',
),
}),
});
export type LoadSkillInput = z.infer<typeof loadSkillInputSchema>['input'];
export type LoadSkillResult = {
skills: Array<{
name: string;
label: string;
content: string;
}>;
message: string;
};
export type LoadSkillFunction = (names: string[]) => Skill[];
export const createLoadSkillTool = (loadSkills: LoadSkillFunction) => ({
description:
'Load specialized skills/expertise by name. Returns detailed instructions for workflows, data manipulation, dashboards, metadata, or research.',
inputSchema: loadSkillInputSchema,
execute: async (parameters: {
input: LoadSkillInput;
}): Promise<LoadSkillResult> => {
const { skillNames } = parameters.input;
const skills = loadSkills(skillNames);
if (skills.length === 0) {
return {
skills: [],
message: `No skills found with names: ${skillNames.join(', ')}. Available skills: workflow-building, data-manipulation, dashboard-building, metadata-building, research.`,
};
}
return {
skills: skills.map((skill) => ({
name: skill.name,
label: skill.label,
content: skill.content,
})),
message: `Loaded ${skills.length} skill(s). Use the instructions above to guide your approach.`,
};
},
});
@@ -7,17 +7,18 @@ import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-
import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity';
import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module';
import { FileModule } from 'src/engine/core-modules/file/file.module';
import { SkillsModule } from 'src/engine/core-modules/skills/skills.module';
import { ThrottlerModule } from 'src/engine/core-modules/throttler/throttler.module';
import { ToolProviderModule } from 'src/engine/core-modules/tool-provider/tool-provider.module';
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module';
import { AiAgentExecutionModule } from 'src/engine/metadata-modules/ai/ai-agent-execution/ai-agent-execution.module';
import { AiAgentModule } from 'src/engine/metadata-modules/ai/ai-agent/ai-agent.module';
import { AiBillingModule } from 'src/engine/metadata-modules/ai/ai-billing/ai-billing.module';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
import { WorkflowToolsModule } from 'src/modules/workflow/workflow-tools/workflow-tools.module';
import { AgentChatController } from './controllers/agent-chat.controller';
import { AgentChatThreadEntity } from './entities/agent-chat-thread.entity';
@@ -34,13 +35,13 @@ import { ChatExecutionService } from './services/chat-execution.service';
FileEntity,
UserWorkspaceEntity,
]),
AiAgentModule,
AiAgentExecutionModule,
ThrottlerModule,
FeatureFlagModule,
FileUploadModule,
FileModule,
PermissionsModule,
SkillsModule,
WorkspaceCacheStorageModule,
WorkspaceCacheModule,
WorkspaceDomainsModule,
@@ -49,6 +50,7 @@ import { ChatExecutionService } from './services/chat-execution.service';
UserWorkspaceModule,
AiBillingModule,
ToolProviderModule,
WorkflowToolsModule,
],
controllers: [AgentChatController],
providers: [
@@ -15,22 +15,21 @@ import { AppPath } from 'twenty-shared/types';
import { getAppPath } from 'twenty-shared/utils';
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
import { SkillsService } from 'src/engine/core-modules/skills/skills.service';
import {
type ToolIndexEntry,
ToolRegistryService,
} from 'src/engine/core-modules/tool-provider/services/tool-registry.service';
import {
AGENT_SEARCH_TOOL_NAME,
createAgentSearchTool,
createLoadSkillTool,
createLoadToolsTool,
type DynamicToolStore,
LOAD_SKILL_TOOL_NAME,
LOAD_TOOLS_TOOL_NAME,
} from 'src/engine/core-modules/tool-provider/tools';
import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { AgentActorContextService } from 'src/engine/metadata-modules/ai/ai-agent-execution/services/agent-actor-context.service';
import { AgentService } from 'src/engine/metadata-modules/ai/ai-agent/agent.service';
import { AGENT_CONFIG } from 'src/engine/metadata-modules/ai/ai-agent/constants/agent-config.const';
import { type AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
import { type BrowsingContextType } from 'src/engine/metadata-modules/ai/ai-agent/types/browsingContext.type';
import { repairToolCall } from 'src/engine/metadata-modules/ai/ai-agent/utils/repair-tool-call.util';
import { AIBillingService } from 'src/engine/metadata-modules/ai/ai-billing/services/ai-billing.service';
@@ -49,11 +48,8 @@ export type ChatExecutionOptions = {
export type ChatExecutionResult = {
stream: ReturnType<typeof streamText>;
preloadedTools: string[];
initialAgents: string[];
};
const INITIAL_AGENTS_LIMIT = 2;
// Common tools to pre-load for quick access
const COMMON_PRELOAD_TOOLS = ['http_request', 'search_help_center'];
@@ -63,7 +59,7 @@ export class ChatExecutionService {
constructor(
private readonly toolRegistry: ToolRegistryService,
private readonly agentService: AgentService,
private readonly skillsService: SkillsService,
private readonly aiModelRegistryService: AiModelRegistryService,
private readonly aiBillingService: AIBillingService,
private readonly agentActorContextService: AgentActorContextService,
@@ -84,21 +80,19 @@ export class ChatExecutionService {
const toolContext = { workspaceId: workspace.id, roleId, actorContext };
const lastUserMessage = this.getLastUserMessage(messages);
const contextString = browsingContext
? this.buildContextFromBrowsingContext(workspace, browsingContext)
: undefined;
const [toolCatalog, initialAgents] = await Promise.all([
this.toolRegistry.buildToolIndex(workspace.id, roleId),
this.agentService.searchAgents(lastUserMessage, workspace.id, {
limit: INITIAL_AGENTS_LIMIT,
}),
]);
const toolCatalog = await this.toolRegistry.buildToolIndex(
workspace.id,
roleId,
);
const skillCatalog = this.skillsService.getAllSkills();
this.logger.log(
`Built tool catalog with ${toolCatalog.length} tools, ${initialAgents.length} agents`,
`Built tool catalog with ${toolCatalog.length} tools, ${skillCatalog.length} skills available`,
);
const preloadedTools = await this.toolRegistry.getToolsByName(
@@ -132,14 +126,14 @@ export class ChatExecutionService {
this.logger.log(`Dynamically loaded tools: ${toolNames.join(', ')}`);
},
),
[AGENT_SEARCH_TOOL_NAME]: createAgentSearchTool((query, options) =>
this.agentService.searchAgents(query, workspace.id, options),
[LOAD_SKILL_TOOL_NAME]: createLoadSkillTool((skillNames) =>
this.skillsService.getSkillsByNames(skillNames),
),
};
const systemPrompt = this.buildSystemPrompt(
toolCatalog,
initialAgents,
skillCatalog,
preloadedToolNames,
contextString,
);
@@ -187,7 +181,6 @@ export class ChatExecutionService {
return {
stream,
preloadedTools: preloadedToolNames,
initialAgents: initialAgents.map((a) => a.name),
};
}
@@ -247,27 +240,9 @@ export class ChatExecutionService {
return context;
}
private getLastUserMessage(
messages: UIMessage<unknown, UIDataTypes, UITools>[],
): string {
for (let i = messages.length - 1; i >= 0; i--) {
const message = messages[i];
if (message.role === 'user') {
const textPart = message.parts.find((part) => part.type === 'text');
if (textPart && 'text' in textPart) {
return textPart.text;
}
}
}
return '';
}
private buildSystemPrompt(
toolCatalog: ToolIndexEntry[],
agents: AgentEntity[],
skillCatalog: Array<{ name: string; label: string; description: string }>,
preloadedTools: string[],
contextString?: string,
): string {
@@ -276,15 +251,8 @@ export class ChatExecutionService {
CHAT_SYSTEM_PROMPTS.RESPONSE_FORMAT,
];
if (agents.length > 0) {
const skillsSection = agents
.map((agent) => `## ${agent.label} Expertise\n${agent.prompt}`)
.join('\n\n');
parts.push(`\nYou have the following expertise:\n\n${skillsSection}`);
}
parts.push(this.buildToolCatalogSection(toolCatalog, preloadedTools));
parts.push(this.buildSkillCatalogSection(skillCatalog));
if (contextString) {
parts.push(
@@ -295,6 +263,26 @@ export class ChatExecutionService {
return parts.join('\n');
}
private buildSkillCatalogSection(
skillCatalog: Array<{ name: string; label: string; description: string }>,
): string {
if (skillCatalog.length === 0) {
return '';
}
const skillsList = skillCatalog
.map((skill) => `- \`${skill.name}\`: ${skill.description}`)
.join('\n');
return `
## Available Skills
Skills provide detailed expertise for specialized tasks. Load a skill before attempting complex operations.
To load a skill, call \`${LOAD_SKILL_TOOL_NAME}\` with the skill name(s).
${skillsList}`;
}
private buildToolCatalogSection(
toolCatalog: ToolIndexEntry[],
preloadedTools: string[],
@@ -357,8 +345,7 @@ ${tools
### How to Use Tools
1. **Web search** (\`web_search\`): Use for ANY request requiring current/real-time information from the internet
2. **Pre-loaded tools** (marked with ✓): Use directly
3. **Other tools**: First call \`${LOAD_TOOLS_TOOL_NAME}({toolNames: ["tool_name"]})\`, then use the tool
4. **Agent expertise**: Call \`${AGENT_SEARCH_TOOL_NAME}\` to load specialized knowledge for workflows, etc.`);
3. **Other tools**: First call \`${LOAD_TOOLS_TOOL_NAME}({toolNames: ["tool_name"]})\`, then use the tool`);
return sections.join('\n');
}
@@ -146,7 +146,7 @@ export class FieldMetadataToolsFactory {
generateTools(workspaceId: string): ToolSet {
return {
'get-field-metadata': {
get_field_metadata: {
description:
'Find fields metadata. Retrieve information about the fields of objects in the workspace data model.',
inputSchema: GetFieldMetadataInputSchema,
@@ -169,7 +169,7 @@ export class FieldMetadataToolsFactory {
});
},
},
'create-field-metadata': {
create_field_metadata: {
description:
'Create a new field metadata on an object. Specify the objectMetadataId and field properties.',
inputSchema: CreateFieldMetadataInputSchema,
@@ -209,7 +209,7 @@ export class FieldMetadataToolsFactory {
}
},
},
'update-field-metadata': {
update_field_metadata: {
description:
'Update an existing field metadata. Provide the field ID and the properties to update.',
inputSchema: UpdateFieldMetadataInputSchema,
@@ -249,7 +249,7 @@ export class FieldMetadataToolsFactory {
}
},
},
'delete-field-metadata': {
delete_field_metadata: {
description: 'Delete a field metadata by its ID.',
inputSchema: DeleteFieldMetadataInputSchema,
execute: async (parameters: { input: { id: string } }) => {
@@ -123,7 +123,7 @@ export class ObjectMetadataToolsFactory {
generateTools(workspaceId: string): ToolSet {
return {
'get-object-metadata': {
get_object_metadata: {
description:
'Find objects metadata. Retrieve information about the data model objects in the workspace.',
inputSchema: GetObjectMetadataInputSchema,
@@ -146,7 +146,7 @@ export class ObjectMetadataToolsFactory {
);
},
},
'create-object-metadata': {
create_object_metadata: {
description:
'Create a new object metadata in the workspace data model.',
inputSchema: CreateObjectMetadataInputSchema,
@@ -183,7 +183,7 @@ export class ObjectMetadataToolsFactory {
}
},
},
'update-object-metadata': {
update_object_metadata: {
description:
'Update an existing object metadata. Provide the object ID and the fields to update.',
inputSchema: UpdateObjectMetadataInputSchema,
@@ -223,7 +223,7 @@ export class ObjectMetadataToolsFactory {
}
},
},
'delete-object-metadata': {
delete_object_metadata: {
description:
'Delete an object metadata by its ID. This will also delete all associated fields.',
inputSchema: DeleteObjectMetadataInputSchema,
@@ -1,70 +0,0 @@
import { DEFAULT_SMART_MODEL } from 'src/engine/metadata-modules/ai/ai-models/constants/ai-models.const';
import { type StandardAgentDefinition } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-agents/types/standard-agent-definition.interface';
import { DASHBOARD_MANAGER_ROLE } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-roles/roles/dashboard-manager-role';
export const DASHBOARD_BUILDER_AGENT: StandardAgentDefinition = {
standardId: '20202020-0002-0001-0001-000000000006',
name: 'dashboard-builder',
label: 'Dashboard Builder',
description: 'AI agent specialized in creating and managing dashboards',
icon: 'IconLayoutDashboard',
applicationId: null,
prompt: `You are a Dashboard Builder Agent for Twenty. You help users create and manage dashboards with widgets.
Capabilities:
- Create new dashboards from scratch
- Add, modify, and remove widgets from dashboards
- Configure widget types (VIEW, GRAPH, FIELDS, TIMELINE, TASKS, NOTES, FILES, EMAILS, CALENDAR, RICH_TEXT, IFRAME, WORKFLOW)
- Manage dashboard tabs and layouts
- Position widgets in a grid system (12-column layout)
Dashboard structure:
- Dashboard: Container with a title and pageLayout
- PageLayout: Contains tabs (type: DASHBOARD)
- PageLayoutTab: Contains widgets with a title, position, and layoutMode (grid/vertical-list/canvas)
- PageLayoutWidget: Individual widget with type, title, gridPosition, and optional configuration
Grid system:
- 12 columns total
- Grid positions: { row, column, rowSpan, columnSpan }
- Common sizes: Full width (columnSpan: 12), Half width (columnSpan: 6), Quarter width (columnSpan: 3)
- Typical heights: Small (rowSpan: 4), Medium (rowSpan: 6), Large (rowSpan: 8)
Widget types explained:
- VIEW: Display a filtered view of records (companies, people, opportunities, etc.)
- GRAPH: Show charts and visualizations of data
- FIELDS: Display specific fields from a record
- TIMELINE: Show activity timeline
- TASKS: Display tasks list
- NOTES: Show notes
- FILES: Display file attachments
- EMAILS: Show email communications
- CALENDAR: Display calendar events
- RICH_TEXT: Custom text content
- IFRAME: Embed external content
- WORKFLOW: Display workflow information
Approach:
- Ask clarifying questions about dashboard purpose and desired widgets
- Suggest appropriate widget types and layouts for the use case
- Create well-organized, visually balanced dashboards
- For modifications, first understand current structure
- Explain widget placement and purpose
- Consider responsive design (widgets stack on smaller screens)
Layout best practices:
- Place most important information at the top
- Group related widgets together
- Use consistent widget sizes when possible
- Leave some whitespace for visual clarity
- Consider logical reading order (left to right, top to bottom)
Prioritize user needs and dashboard usability.`,
modelId: DEFAULT_SMART_MODEL,
responseFormat: { type: 'text' },
isCustom: false,
standardRoleId: DASHBOARD_MANAGER_ROLE.standardId,
modelConfiguration: {},
outputStrategy: 'direct',
evaluationInputs: [],
};
@@ -1,48 +0,0 @@
import { DEFAULT_SMART_MODEL } from 'src/engine/metadata-modules/ai/ai-models/constants/ai-models.const';
import { type StandardAgentDefinition } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-agents/types/standard-agent-definition.interface';
import { DATA_MANIPULATOR_ROLE } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-roles/roles/data-manipulator-role';
export const DATA_MANIPULATOR_AGENT: StandardAgentDefinition = {
standardId: '20202020-0002-0001-0001-000000000003',
name: 'data-manipulator',
label: 'Data Manipulator',
description:
'AI agent specialized in exploring, reading, creating, updating, and managing data across all objects',
icon: 'IconEdit',
applicationId: null,
prompt: `You are a Data Manipulator Agent for Twenty. You explore and manage data across companies, people, opportunities, tasks, notes, and custom objects.
Capabilities:
- Search, filter, sort, create, update records
- Manage relationships between records
- Bulk operations and data analysis
Constraints:
- READ and WRITE access to all objects
- CANNOT delete records or access workflow objects
- CANNOT modify workspace settings
Multi-step approach:
- Chain queries to solve complex requests (e.g., find companies → get their opportunities → calculate totals)
- If a query fails or returns no results, try alternative filters or approaches
- Validate data exists before referencing it (search before update)
- Use results from one query to inform the next
- Try 2-3 different approaches before giving up
Sorting (critical):
- For "top N" queries, use orderBy with limit
- Examples: orderBy: [{"employees": "DescNullsLast"}], orderBy: [{"createdAt": "AscNullsFirst"}]
- Valid directions: "AscNullsFirst", "AscNullsLast", "DescNullsFirst", "DescNullsLast"
Before bulk operations:
- Confirm the scope and impact
- Explain what will change
Prioritize data integrity and provide clear feedback on operations performed.`,
modelId: DEFAULT_SMART_MODEL,
responseFormat: { type: 'text' },
isCustom: false,
standardRoleId: DATA_MANIPULATOR_ROLE.standardId,
modelConfiguration: {},
evaluationInputs: [],
};
@@ -1,80 +0,0 @@
import { DEFAULT_SMART_MODEL } from 'src/engine/metadata-modules/ai/ai-models/constants/ai-models.const';
import { type StandardAgentDefinition } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-agents/types/standard-agent-definition.interface';
import { DATA_MODEL_MANAGER_ROLE } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-roles/roles/data-model-manager-role';
export const METADATA_BUILDER_AGENT: StandardAgentDefinition = {
standardId: '20202020-0002-0001-0001-000000000007',
name: 'metadata-builder',
label: 'Metadata Builder',
description:
'AI agent specialized in modifying the workspace SCHEMA/DATA MODEL - creating new object types, adding fields to objects, and managing object structure (NOT for creating data records)',
icon: 'IconDatabaseEdit',
applicationId: null,
prompt: `You are a Metadata Builder Agent for Twenty. You help users manage their workspace data model by creating, updating, and organizing custom objects and fields.
Capabilities:
- Create new custom objects with appropriate naming and configuration
- Add fields to existing objects (text, number, date, select, relation, etc.)
- Update object and field properties (labels, descriptions, icons)
- Manage field settings (required, unique, default values)
- Create relations between objects
Key concepts:
- Objects: Represent entities in the data model (e.g., Company, Person, Opportunity)
- Fields: Properties of objects with specific types (TEXT, NUMBER, DATE_TIME, SELECT, RELATION, etc.)
- Relations: Links between objects (one-to-many, many-to-one)
- Labels vs Names: Labels are for display, names are internal identifiers (camelCase)
Field types available:
- TEXT: Simple text fields
- NUMBER: Numeric values (integers or decimals)
- BOOLEAN: True/false values
- DATE_TIME: Date and time values
- DATE: Date only values
- SELECT: Single choice from options
- MULTI_SELECT: Multiple choices from options
- LINK: URL fields
- LINKS: Multiple URL fields
- EMAIL: Email address fields
- EMAILS: Multiple email fields
- PHONE: Phone number fields
- PHONES: Multiple phone fields
- CURRENCY: Monetary values
- RATING: Star ratings
- RELATION: Links to other objects
- RICH_TEXT: Formatted text content
Best practices:
- Use clear, descriptive names for objects and fields
- Follow naming conventions: singular for object names, camelCase for field names
- Add helpful descriptions to objects and fields
- Choose appropriate field types for the data being stored
- Consider relationships between objects when designing the data model
Approach:
- Ask clarifying questions to understand the user's data modeling needs
- Suggest best practices for naming and organization
- Explain the impact of changes to the data model
- Verify object and field existence before making updates
- Provide clear feedback on operations performed
Prioritize data model integrity and user understanding.`,
modelId: DEFAULT_SMART_MODEL,
responseFormat: { type: 'text' },
isCustom: false,
standardRoleId: DATA_MODEL_MANAGER_ROLE.standardId,
modelConfiguration: {},
outputStrategy: 'direct',
evaluationInputs: [
'Create a custom object called "Project" with fields for name, description, start date, and status',
'Add a currency field called "budget" to the Project object',
'Create a relation between Project and Company so each project belongs to a company',
'Add a multi-select field for project tags with options: urgent, internal, client-facing',
'Update the Project object description to explain what it tracks',
'Create a "Task" object that relates to both Project and Person',
'Add a rating field to track project priority from 1-5 stars',
'Create a custom object for tracking Invoices with amount, date, and status fields',
'Add a link field to the Company object for their website',
'Create a relation between Person and Company for the account manager',
],
};
@@ -1,43 +0,0 @@
import { DEFAULT_SMART_MODEL } from 'src/engine/metadata-modules/ai/ai-models/constants/ai-models.const';
import { type StandardAgentDefinition } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-agents/types/standard-agent-definition.interface';
export const RESEARCHER_AGENT: StandardAgentDefinition = {
standardId: '20202020-0002-0001-0001-000000000005',
name: 'researcher',
label: 'Researcher',
description:
'AI agent specialized in researching information, finding facts, and gathering data from the web',
icon: 'IconSearch',
applicationId: null,
prompt: `You are a Researcher Agent for Twenty. You find information and gather facts from the web.
Capabilities:
- Search for current information and facts
- Research companies, people, technologies, trends
- Gather competitive intelligence and market data
- Find contact details and verify information
Research strategy:
- Try multiple search queries from different angles
- If initial searches fail, use alternative search terms
- Cross-reference information when possible
- Cite sources and provide context
Present findings:
- Be thorough but concise
- Organize information logically
- Distinguish facts from speculation
- Note if information might be outdated
- Include relevant sources
Be persistent in finding accurate information.`,
modelId: DEFAULT_SMART_MODEL,
responseFormat: { type: 'text' },
isCustom: false,
modelConfiguration: {
webSearch: {
enabled: true,
},
},
evaluationInputs: [],
};
@@ -1,90 +0,0 @@
import { DEFAULT_SMART_MODEL } from 'src/engine/metadata-modules/ai/ai-models/constants/ai-models.const';
import { type StandardAgentDefinition } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-agents/types/standard-agent-definition.interface';
import { WORKFLOW_MANAGER_ROLE } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-roles/roles/workflow-manager-role';
export const WORKFLOW_BUILDER_AGENT: StandardAgentDefinition = {
standardId: '20202020-0002-0001-0001-000000000001',
name: 'workflow-builder',
label: 'Workflow Builder',
description: 'AI agent specialized in creating and managing workflows',
icon: 'IconSettingsAutomation',
applicationId: null,
prompt: `You are a Workflow Builder Agent for Twenty. You help users create and manage automation workflows.
Capabilities:
- Create workflows from scratch
- Modify existing workflows (add, remove, update steps)
- Explain workflow structure and suggest improvements
Key concepts:
- Triggers: DATABASE_EVENT, MANUAL, CRON, WEBHOOK
- Steps: CREATE_RECORD, SEND_EMAIL, CODE, etc.
- Data flow: Use {{stepId.fieldName}} to reference previous step outputs
- Relationships: Use nested objects like {"company": {"id": "{{reference}}"}}
CRON Trigger Settings Schema:
For CRON triggers, settings.type must be one of these exact values:
1. "DAYS" - Daily schedule
- Requires: schedule: { day: number (1+), hour: number (0-23), minute: number (0-59) }
- Example: { type: "DAYS", schedule: { day: 1, hour: 9, minute: 0 }, outputSchema: {} }
2. "HOURS" - Hourly schedule (USE THIS FOR "EVERY HOUR")
- Requires: schedule: { hour: number (1+), minute: number (0-59) }
- Example: { type: "HOURS", schedule: { hour: 1, minute: 0 }, outputSchema: {} }
- This runs every X hours at Y minutes past the hour
3. "MINUTES" - Minute-based schedule
- Requires: schedule: { minute: number (1+) }
- Example: { type: "MINUTES", schedule: { minute: 15 }, outputSchema: {} }
4. "CUSTOM" - Custom cron pattern
- Requires: pattern: string (cron expression)
- Example: { type: "CUSTOM", pattern: "0 * * * *", outputSchema: {} }
Critical: Always rely on tool schema definitions
- The workflow creation tool provides comprehensive schemas with examples
- Follow schema definitions exactly for field names, types, and structures
- Schema includes validation rules and common patterns
Approach:
- Ask clarifying questions to understand user needs
- Suggest appropriate actions for the use case
- Explain each step and why it's needed
- For modifications, understand current structure first
- Ensure workflow logic remains coherent
Prioritize user understanding and workflow effectiveness.`,
modelId: DEFAULT_SMART_MODEL,
responseFormat: { type: 'text' },
isCustom: false,
standardRoleId: WORKFLOW_MANAGER_ROLE.standardId,
modelConfiguration: {},
outputStrategy: 'direct',
evaluationInputs: [
'Build a workflow that runs everyday at 9:00 AM, finds the companies added in the last 24 hours, and create task title Welcome {companyName} for each',
'Create a workflow that listens to company creation events and makes an http call to companies.twenty.com/{domain} to enrich them',
'Update the quick lead workflow to add an http request to Google.com as the last step',
'when a new lead is created, automatically send an email to the sales team with the lead details',
'I need a workflow that runs every monday morning and creates a weekly summary report of all closed deals',
'can you make a workflow to automatically assign new oppurtunities to sales reps based on territory?',
'create workflow that updates contact status to inactive if theres no activity for 90 days',
'Build automation to send followup email 3 days after first contact with prospect',
'i want to automatically create a task for account manager when deal reaches negotiation stage',
'setup a workflow that enriches company data from clearbit when new account is created',
'make a workflow to notify slack channel when deal amount is over $50k',
'need workflow that runs daily and finds all overdue tasks then sends reminder emails',
'Create automation to update lead score when contact opens email or clicks link',
'workflow to automatically create renewal opportunity 60 days before contract end date',
'can you build a flow that copies contact info to company record when deal is won?',
'I need to send a survey email 7 days after deal closes',
'make workflow that assigns leads round-robin style to available sales reps',
'create automation to tag contacts as "hot lead" when they visit pricing page 3 times',
'workflow that escalates support tickets to manager if not resolved in 48 hours',
'need a workflow to sync new contacts to mailchimp mailing list',
'build flow that updates deal stage to lost if no activity for 30 days',
'create workflow that sends birthday email to contacts on thier birthday',
'can i get a workflow that creates calendar event when meeting is scheduled with prospect',
'workflow to automatically generate quote pdf when opportunity moves to proposal stage',
],
};
@@ -1,16 +1,6 @@
import { DASHBOARD_BUILDER_AGENT } from './agents/dashboard-builder-agent';
import { DATA_MANIPULATOR_AGENT } from './agents/data-manipulator-agent';
import { HELPER_AGENT } from './agents/helper-agent';
import { METADATA_BUILDER_AGENT } from './agents/metadata-builder-agent';
import { RESEARCHER_AGENT } from './agents/researcher-agent';
import { WORKFLOW_BUILDER_AGENT } from './agents/workflow-builder-agent';
import { type StandardAgentDefinition } from './types/standard-agent-definition.interface';
export const STANDARD_AGENT_DEFINITIONS = [
WORKFLOW_BUILDER_AGENT,
DATA_MANIPULATOR_AGENT,
DASHBOARD_BUILDER_AGENT,
HELPER_AGENT,
RESEARCHER_AGENT,
METADATA_BUILDER_AGENT,
] as const satisfies StandardAgentDefinition[];
@@ -177,6 +177,13 @@ This is the most efficient way for AI to create workflows as it handles all the
trigger: parameters.trigger,
steps: parameters.steps,
},
recordReferences: [
{
objectNameSingular: 'workflow',
recordId: workflowId,
displayName: parameters.name,
},
],
};
} catch (error) {
return {
@@ -1,7 +1,8 @@
import { Module } from '@nestjs/common';
import { Global, Module } from '@nestjs/common';
import { RecordPositionModule } from 'src/engine/core-modules/record-position/record-position.module';
import { ToolGeneratorModule } from 'src/engine/core-modules/tool-generator/tool-generator.module';
import { WORKFLOW_TOOL_SERVICE_TOKEN } from 'src/engine/core-modules/tool-provider/constants/workflow-tool-service.token';
import { WorkflowSchemaModule } from 'src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.module';
import { WorkflowVersionEdgeModule } from 'src/modules/workflow/workflow-builder/workflow-version-edge/workflow-version-edge.module';
import { WorkflowVersionStepModule } from 'src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step.module';
@@ -10,6 +11,9 @@ import { WorkflowTriggerModule } from 'src/modules/workflow/workflow-trigger/wor
import { WorkflowToolWorkspaceService } from './services/workflow-tool.workspace-service';
// Global module to make WORKFLOW_TOOL_SERVICE_TOKEN available to ToolProviderModule
// without creating a circular dependency (ToolProviderModule cannot import this module directly)
@Global()
@Module({
imports: [
WorkflowVersionStepModule,
@@ -20,7 +24,13 @@ import { WorkflowToolWorkspaceService } from './services/workflow-tool.workspace
RecordPositionModule,
ToolGeneratorModule,
],
providers: [WorkflowToolWorkspaceService],
exports: [WorkflowToolWorkspaceService],
providers: [
WorkflowToolWorkspaceService,
{
provide: WORKFLOW_TOOL_SERVICE_TOKEN,
useExisting: WorkflowToolWorkspaceService,
},
],
exports: [WorkflowToolWorkspaceService, WORKFLOW_TOOL_SERVICE_TOKEN],
})
export class WorkflowToolsModule {}
@@ -16,7 +16,7 @@ exports[`Agent deletion should fail should fail when attempting to delete a stan
],
"flatEntityMinimalInformation": {
"id": Any<String>,
"name": "dashboard-builder",
"name": "helper",
},
"status": "fail",
"type": "delete_agent",
@@ -50,16 +50,16 @@ describe('Agent deletion should fail', () => {
gqlFields: 'id name isCustom',
});
const dashboardBuilderAgent = data.findManyAgents.find(
(agent) => agent.name === 'dashboard-builder' && agent.isCustom === false,
const helperAgent = data.findManyAgents.find(
(agent) => agent.name === 'helper' && agent.isCustom === false,
);
expect(dashboardBuilderAgent).toBeDefined();
expect(helperAgent).toBeDefined();
const { errors } = await deleteOneAgent({
expectToFail: true,
input: {
id: dashboardBuilderAgent!.id,
id: helperAgent!.id,
},
});
@@ -205,16 +205,16 @@ describe('Agent update should fail', () => {
gqlFields: 'id name isCustom',
});
const dashboardBuilderAgent = data.findManyAgents.find(
(agent) => agent.name === 'dashboard-builder' && agent.isCustom === false,
const helperAgent = data.findManyAgents.find(
(agent) => agent.name === 'helper' && agent.isCustom === false,
);
expect(dashboardBuilderAgent).toBeDefined();
expect(helperAgent).toBeDefined();
const { errors } = await updateOneAgent({
expectToFail: true,
input: {
id: dashboardBuilderAgent!.id,
id: helperAgent!.id,
label: 'Attempted Update to Standard Agent',
},
});