Add preconfigured Workflow creation agent (#13855)

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
This commit is contained in:
Abdul Rahman
2025-08-27 17:50:38 +05:30
committed by GitHub
parent 28de78bfc0
commit 9098df7ddf
50 changed files with 1949 additions and 339 deletions
@@ -62,6 +62,7 @@ export type Agent = {
prompt: Scalars['String'];
responseFormat?: Maybe<Scalars['JSON']>;
roleId?: Maybe<Scalars['UUID']>;
standardId?: Maybe<Scalars['UUID']>;
updatedAt: Scalars['DateTime'];
};
@@ -62,6 +62,7 @@ export type Agent = {
prompt: Scalars['String'];
responseFormat?: Maybe<Scalars['JSON']>;
roleId?: Maybe<Scalars['UUID']>;
standardId?: Maybe<Scalars['UUID']>;
updatedAt: Scalars['DateTime'];
};
@@ -1,6 +1,6 @@
import { type WorkflowRunState } from '@/workflow/types/Workflow';
import { workflowRunStateSchema } from '@/workflow/validation-schemas/workflowSchema';
import { isDefined } from 'twenty-shared/utils';
import { workflowRunStateSchema } from 'twenty-shared/workflow';
import { type JsonValue } from 'type-fest';
export const orderWorkflowRunState = (value: JsonValue) => {
@@ -1,9 +1,9 @@
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
import { type WorkflowRun } from '@/workflow/types/Workflow';
import { workflowRunSchema } from '@/workflow/validation-schemas/workflowSchema';
import { useMemo } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { workflowRunSchema } from 'twenty-shared/workflow';
export const useWorkflowRun = ({
workflowRunId,
@@ -18,7 +18,7 @@ import {
type workflowTriggerSchema,
type workflowUpdateRecordActionSchema,
type workflowWebhookTriggerSchema,
} from '@/workflow/validation-schemas/workflowSchema';
} from 'twenty-shared/workflow';
import { type z } from 'zod';
export type WorkflowCodeAction = z.infer<typeof workflowCodeActionSchema>;
@@ -1,6 +1,7 @@
import { type WorkflowFilterAction } from '@/workflow/types/Workflow';
import { type Meta, type StoryObj } from '@storybook/react';
import { fn } from '@storybook/test';
import { StepLogicalOperator, ViewFilterOperand } from 'twenty-shared/types';
import { ComponentDecorator } from 'twenty-ui/testing';
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
import { WorkflowStepActionDrawerDecorator } from '~/testing/decorators/WorkflowStepActionDrawerDecorator';
@@ -42,9 +43,7 @@ const CONFIGURED_ACTION: WorkflowFilterAction = {
stepFilterGroups: [
{
id: 'filter-group-1',
parentStepFilterGroupId: null,
logicalOperator: 'AND',
stepFilterGroupChildren: [],
logicalOperator: StepLogicalOperator.AND,
},
],
stepFilters: [
@@ -52,10 +51,9 @@ const CONFIGURED_ACTION: WorkflowFilterAction = {
id: 'filter-1',
stepFilterGroupId: 'filter-group-1',
stepOutputKey: 'company.name',
displayValue: 'Company Name',
operandType: 'LITERAL',
operand: 'contains',
operand: ViewFilterOperand.Contains,
value: 'Acme',
type: 'string',
},
],
},
@@ -3,13 +3,13 @@ import {
type WorkflowHttpRequestAction,
type WorkflowSendEmailAction,
} from '@/workflow/types/Workflow';
import { renderHook } from '@testing-library/react';
import { FieldMetadataType } from 'twenty-shared/types';
import {
workflowFormActionSettingsSchema,
workflowHttpRequestActionSettingsSchema,
workflowSendEmailActionSettingsSchema,
} from '@/workflow/validation-schemas/workflowSchema';
import { renderHook } from '@testing-library/react';
import { FieldMetadataType } from 'twenty-shared/types';
} from 'twenty-shared/workflow';
import { useWorkflowActionHeader } from '../useWorkflowActionHeader';
jest.mock('../useActionIconColorOrThrow', () => ({
@@ -0,0 +1,15 @@
import { type MigrationInterface, type QueryRunner } from 'typeorm';
export class AddStandardIdToAgent1754923039348 implements MigrationInterface {
name = 'AddStandardIdToAgent1754923039348';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "core"."agent" ADD "standardId" uuid`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."agent" DROP COLUMN "standardId"`,
);
}
}
@@ -182,4 +182,39 @@ export class AiModelRegistryService {
refreshRegistry(): void {
this.buildModelRegistry();
}
async resolveModelForAgent(agent: { modelId: string } | null) {
const aiModel = this.getEffectiveModelConfig(agent?.modelId ?? 'auto');
await this.validateApiKey(aiModel.provider);
const registeredModel = this.getModel(aiModel.modelId);
if (!registeredModel) {
throw new Error(`Model ${aiModel.modelId} not found in registry`);
}
return registeredModel;
}
async validateApiKey(provider: ModelProvider): Promise<void> {
let apiKey: string | undefined;
switch (provider) {
case ModelProvider.OPENAI:
apiKey = this.twentyConfigService.get('OPENAI_API_KEY');
break;
case ModelProvider.ANTHROPIC:
apiKey = this.twentyConfigService.get('ANTHROPIC_API_KEY');
break;
case ModelProvider.OPENAI_COMPATIBLE:
apiKey = this.twentyConfigService.get('OPENAI_COMPATIBLE_API_KEY');
break;
default:
return;
}
if (!apiKey) {
throw new Error(`${provider.toUpperCase()} API key not configured`);
}
}
}
@@ -11,7 +11,7 @@ import {
generateSoftDeleteToolSchema,
getRecordInputSchema,
} from 'src/engine/metadata-modules/agent/utils/agent-tool-schema.utils';
import { isWorkflowRelatedObject } from 'src/engine/metadata-modules/agent/utils/is-workflow-related-object.util';
import { isWorkflowRunObject } from 'src/engine/metadata-modules/agent/utils/is-workflow-run-object.util';
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
import { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util';
import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service';
@@ -48,7 +48,7 @@ export class ToolService {
});
const filteredObjectMetadata = allObjectMetadata.filter(
(objectMetadata) => !isWorkflowRelatedObject(objectMetadata),
(objectMetadata) => !isWorkflowRunObject(objectMetadata),
);
filteredObjectMetadata.forEach((objectMetadata) => {
@@ -11,23 +11,22 @@ import {
generateText,
type ImagePart,
streamText,
ToolSet,
type UserContent,
} from 'ai';
import { In, Repository } from 'typeorm';
import { ModelProvider } from 'src/engine/core-modules/ai/constants/ai-models.const';
import { AiModelRegistryService } from 'src/engine/core-modules/ai/services/ai-model-registry.service';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity';
import { FileService } from 'src/engine/core-modules/file/services/file.service';
import { extractFolderPathAndFilename } from 'src/engine/core-modules/file/utils/extract-folderpath-and-filename.utils';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { type Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import {
type AgentChatMessageEntity,
AgentChatMessageRole,
} from 'src/engine/metadata-modules/agent/agent-chat-message.entity';
import { AgentToolService } from 'src/engine/metadata-modules/agent/agent-tool.service';
import { AgentHandoffToolService } from 'src/engine/metadata-modules/agent/agent-handoff-tool.service';
import { AGENT_CONFIG } from 'src/engine/metadata-modules/agent/constants/agent-config.const';
import { AGENT_SYSTEM_PROMPTS } from 'src/engine/metadata-modules/agent/constants/agent-system-prompts.const';
import { type RecordIdsByObjectMetadataNameSingularType } from 'src/engine/metadata-modules/agent/types/recordIdsByObjectMetadataNameSingular.type';
@@ -37,6 +36,7 @@ import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.
import { type OutputSchema } from 'src/modules/workflow/workflow-builder/workflow-schema/types/output-schema.type';
import { streamToBuffer } from 'src/utils/stream-to-buffer';
import { AgentToolGeneratorService } from './agent-tool-generator.service';
import { AgentEntity } from './agent.entity';
import { AgentException, AgentExceptionCode } from './agent.exception';
@@ -54,40 +54,20 @@ export class AgentExecutionService {
private readonly logger = new Logger(AgentExecutionService.name);
constructor(
private readonly twentyConfigService: TwentyConfigService,
private readonly agentToolService: AgentToolService,
private readonly agentHandoffToolService: AgentHandoffToolService,
private readonly fileService: FileService,
private readonly domainManagerService: DomainManagerService,
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService,
private readonly aiModelRegistryService: AiModelRegistryService,
private readonly agentToolGeneratorService: AgentToolGeneratorService,
@InjectRepository(AgentEntity, 'core')
private readonly agentRepository: Repository<AgentEntity>,
@InjectRepository(FileEntity, 'core')
private readonly fileRepository: Repository<FileEntity>,
) {}
private async validateApiKey(provider: ModelProvider): Promise<void> {
let apiKey: string | undefined;
switch (provider) {
case ModelProvider.OPENAI:
apiKey = this.twentyConfigService.get('OPENAI_API_KEY');
break;
case ModelProvider.ANTHROPIC:
apiKey = this.twentyConfigService.get('ANTHROPIC_API_KEY');
break;
default:
return;
}
if (!apiKey) {
throw new AgentException(
`${provider.toUpperCase()} API key not configured`,
AgentExceptionCode.API_KEY_NOT_CONFIGURED,
);
}
}
async prepareAIRequestConfig({
messages,
prompt,
@@ -106,48 +86,29 @@ export class AgentExecutionService {
);
}
const aiModel = this.aiModelRegistryService.getEffectiveModelConfig(
agent?.modelId ?? 'auto',
);
const registeredModel =
await this.aiModelRegistryService.resolveModelForAgent(agent);
if (agent && !aiModel) {
const error = `AI model with id ${agent.modelId} not found`;
let tools: ToolSet = {};
this.logger.error(error);
throw new AgentException(
error,
AgentExceptionCode.AGENT_EXECUTION_FAILED,
);
}
this.logger.log(
`Resolved model: ${aiModel.modelId} (provider: ${aiModel.provider})`,
);
const provider = aiModel.provider;
await this.validateApiKey(provider);
const tools = agent
? await this.agentToolService.generateToolsForAgent(
if (agent) {
const baseTools =
await this.agentToolGeneratorService.generateToolsForAgent(
agent.id,
agent.workspaceId,
)
: {};
);
const handoffTools =
await this.agentHandoffToolService.generateHandoffTools(
agent.id,
agent.workspaceId,
);
tools = { ...baseTools, ...handoffTools };
}
this.logger.log(`Generated ${Object.keys(tools).length} tools for agent`);
const registeredModel = this.aiModelRegistryService.getModel(
aiModel.modelId,
);
if (!registeredModel) {
throw new AgentException(
`Model ${aiModel.modelId} not found in registry`,
AgentExceptionCode.AGENT_EXECUTION_FAILED,
);
}
return {
system,
tools,
@@ -1,13 +1,13 @@
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { generateText } from 'ai';
import { CoreMessage, generateText } from 'ai';
import { Repository } from 'typeorm';
import { AiModelRegistryService } from 'src/engine/core-modules/ai/services/ai-model-registry.service';
import { AGENT_HANDOFF_PROMPT_TEMPLATE } from 'src/engine/metadata-modules/agent/constants/agent-handoff-prompt.const';
import { AgentHandoffService } from './agent-handoff.service';
import { AgentToolGeneratorService } from './agent-tool-generator.service';
import { AgentEntity } from './agent.entity';
import { AgentException, AgentExceptionCode } from './agent.exception';
@@ -15,8 +15,7 @@ export type HandoffRequest = {
fromAgentId: string;
toAgentId: string;
workspaceId: string;
reason: string;
context?: string;
messages?: CoreMessage[];
};
@Injectable()
@@ -28,11 +27,12 @@ export class AgentHandoffExecutorService {
private readonly agentRepository: Repository<AgentEntity>,
private readonly agentHandoffService: AgentHandoffService,
private readonly aiModelRegistryService: AiModelRegistryService,
private readonly agentToolGeneratorService: AgentToolGeneratorService,
) {}
async executeHandoff(handoffRequest: HandoffRequest) {
try {
const { fromAgentId, toAgentId, workspaceId } = handoffRequest;
const { fromAgentId, toAgentId, workspaceId, messages } = handoffRequest;
const canHandoff = await this.agentHandoffService.canHandoffTo({
fromAgentId,
@@ -58,9 +58,8 @@ export class AgentHandoffExecutorService {
);
}
const registeredModel = this.aiModelRegistryService.getModel(
targetAgent.modelId,
);
const registeredModel =
await this.aiModelRegistryService.resolveModelForAgent(targetAgent);
if (!registeredModel) {
throw new AgentException(
@@ -69,14 +68,24 @@ export class AgentHandoffExecutorService {
);
}
const tools = await this.agentToolGeneratorService.generateToolsForAgent(
toAgentId,
workspaceId,
);
const aiRequestConfig = {
system: targetAgent.prompt,
prompt: this.createHandoffPrompt(handoffRequest),
messages,
tools,
model: registeredModel.model,
};
const textResponse = await generateText(aiRequestConfig);
this.logger.log(
`Successfully executed handoff to agent ${toAgentId} with response length: ${textResponse.text.length}`,
);
return textResponse.text;
} catch (error) {
this.logger.error(
@@ -92,13 +101,4 @@ export class AgentHandoffExecutorService {
};
}
}
private createHandoffPrompt(handoffRequest: HandoffRequest): string {
const { reason, context } = handoffRequest;
return AGENT_HANDOFF_PROMPT_TEMPLATE.replace('{reason}', reason).replace(
'{context}',
context || 'No additional context provided',
);
}
}
@@ -0,0 +1,56 @@
import { Injectable } from '@nestjs/common';
import { type ToolSet } from 'ai';
import { AgentHandoffExecutorService } from 'src/engine/metadata-modules/agent/agent-handoff-executor.service';
import { AgentHandoffService } from 'src/engine/metadata-modules/agent/agent-handoff.service';
import { AGENT_HANDOFF_DESCRIPTION_TEMPLATE } from 'src/engine/metadata-modules/agent/constants/agent-handoff-description.const';
import { AGENT_HANDOFF_SCHEMA } from 'src/engine/metadata-modules/agent/constants/agent-handoff-schema.const';
import { camelCase } from 'src/utils/camel-case';
@Injectable()
export class AgentHandoffToolService {
constructor(
private readonly agentHandoffService: AgentHandoffService,
private readonly agentHandoffExecutorService: AgentHandoffExecutorService,
) {}
public async generateHandoffTools(
agentId: string,
workspaceId: string,
): Promise<ToolSet> {
const handoffs = await this.agentHandoffService.getAgentHandoffs({
fromAgentId: agentId,
workspaceId,
});
const handoffTools = handoffs.reduce<ToolSet>((tools, handoff) => {
const toolName = `handoff_to_${camelCase(handoff.toAgent.name)}`;
tools[toolName] = {
description:
handoff.description ||
handoff.toAgent.description ||
AGENT_HANDOFF_DESCRIPTION_TEMPLATE.replace(
'{agentName}',
handoff.toAgent.name,
),
parameters: AGENT_HANDOFF_SCHEMA,
execute: async ({ input }) => {
const result = await this.agentHandoffExecutorService.executeHandoff({
fromAgentId: agentId,
toAgentId: handoff.toAgent.id,
workspaceId,
messages: input.messages,
});
return result;
},
};
return tools;
}, {});
return handoffTools;
}
}
@@ -0,0 +1,94 @@
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { type ToolSet } from 'ai';
import { Repository } from 'typeorm';
import { ToolAdapterService } from 'src/engine/core-modules/ai/services/tool-adapter.service';
import { ToolService } from 'src/engine/core-modules/ai/services/tool.service';
import { AgentService } from 'src/engine/metadata-modules/agent/agent.service';
import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants';
import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { WorkflowToolWorkspaceService as WorkflowToolService } from 'src/modules/workflow/workflow-tools/services/workflow-tool.workspace-service';
@Injectable()
export class AgentToolGeneratorService {
private readonly logger = new Logger(AgentToolGeneratorService.name);
constructor(
@InjectRepository(RoleEntity, 'core')
private readonly roleRepository: Repository<RoleEntity>,
private readonly toolAdapterService: ToolAdapterService,
private readonly toolService: ToolService,
private readonly workflowToolService: WorkflowToolService,
private readonly permissionsService: PermissionsService,
private readonly agentService: AgentService,
) {}
async generateToolsForAgent(
agentId: string,
workspaceId: string,
): Promise<ToolSet> {
let tools: ToolSet = {};
try {
const agent = await this.agentService.findOneAgent(agentId, workspaceId);
const actionTools = await this.toolAdapterService.getTools();
tools = { ...actionTools };
const roleId = agent.roleId;
if (!roleId) {
return tools;
}
const role = await this.roleRepository.findOne({
where: {
id: roleId,
workspaceId,
},
});
if (!role) {
return tools;
}
const hasWorkflowPermission =
this.permissionsService.checkRolePermissions(
role,
PermissionFlagType.WORKFLOWS,
);
if (hasWorkflowPermission) {
const workflowTools = this.workflowToolService.generateWorkflowTools(
workspaceId,
roleId,
);
tools = { ...tools, ...workflowTools };
}
const databaseTools = await this.toolService.listTools(
roleId,
workspaceId,
);
tools = { ...tools, ...databaseTools };
const roleActionTools = await this.toolAdapterService.getTools(
roleId,
workspaceId,
);
tools = { ...tools, ...roleActionTools };
} catch (toolError) {
this.logger.warn(
`Failed to generate tools for agent ${agentId}: ${toolError.message}. Proceeding without tools.`,
);
}
return tools;
}
}
@@ -1,127 +0,0 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { type ToolSet } from 'ai';
import { Repository } from 'typeorm';
import { z } from 'zod';
import { ToolAdapterService } from 'src/engine/core-modules/ai/services/tool-adapter.service';
import { ToolService } from 'src/engine/core-modules/ai/services/tool.service';
import { AgentHandoffExecutorService } from 'src/engine/metadata-modules/agent/agent-handoff-executor.service';
import { AgentHandoffService } from 'src/engine/metadata-modules/agent/agent-handoff.service';
import { AgentService } from 'src/engine/metadata-modules/agent/agent.service';
import { AGENT_HANDOFF_DESCRIPTION_TEMPLATE } from 'src/engine/metadata-modules/agent/constants/agent-handoff-description.const';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { camelCase } from 'src/utils/camel-case';
@Injectable()
export class AgentToolService {
constructor(
private readonly agentService: AgentService,
private readonly agentHandoffService: AgentHandoffService,
private readonly agentHandoffExecutorService: AgentHandoffExecutorService,
@InjectRepository(RoleEntity, 'core')
private readonly roleRepository: Repository<RoleEntity>,
private readonly toolService: ToolService,
private readonly toolAdapterService: ToolAdapterService,
) {}
async generateToolsForAgent(
agentId: string,
workspaceId: string,
): Promise<ToolSet> {
const agent = await this.agentService.findOneAgent(agentId, workspaceId);
const handoffTools = await this.generateHandoffTools(agentId, workspaceId);
if (!agent.roleId) {
const actionTools = await this.toolAdapterService.getTools();
return { ...actionTools, ...handoffTools };
}
const role = await this.roleRepository.findOne({
where: {
id: agent.roleId,
workspaceId,
},
});
if (!role) {
return {};
}
const actionTools = await this.toolAdapterService.getTools(
role.id,
workspaceId,
);
const databaseTools = await this.toolService.listTools(
role.id,
workspaceId,
);
return { ...databaseTools, ...actionTools, ...handoffTools };
}
private async generateHandoffTools(
agentId: string,
workspaceId: string,
): Promise<ToolSet> {
const handoffs = await this.agentHandoffService.getAgentHandoffs({
fromAgentId: agentId,
workspaceId,
});
const handoffTools = handoffs.reduce<ToolSet>((tools, handoff) => {
const toolName = `handoff_to_${camelCase(handoff.toAgent.name)}`;
const handoffSchema = z.object({
toolDescription: z
.string()
.describe(
"A clear, human-readable status message describing the handoff being made. This will be shown to the user while the handoff is being processed, so phrase it as a present-tense status update (e.g., 'Transferring you to the sales agent for pricing information').",
),
input: z.object({
reason: z
.string()
.describe(
'Brief explanation of why this handoff is needed (e.g., "User needs pricing information", "User requires technical support", "User wants to discuss billing")',
),
context: z
.string()
.optional()
.describe(
'Any relevant context or information to pass to the receiving agent (e.g., user preferences, previous conversation details, specific requirements)',
),
}),
});
tools[toolName] = {
description:
handoff.description ||
handoff.toAgent.description ||
AGENT_HANDOFF_DESCRIPTION_TEMPLATE.replace(
'{agentName}',
handoff.toAgent.name,
),
parameters: handoffSchema,
execute: async ({ input: { reason, context } }) => {
const result = await this.agentHandoffExecutorService.executeHandoff({
fromAgentId: agentId,
toAgentId: handoff.toAgent.id,
workspaceId,
reason,
context,
});
return result;
},
};
return tools;
}, {});
return handoffTools;
}
}
@@ -29,6 +29,9 @@ export class AgentEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ nullable: true, type: 'uuid' })
standardId?: string;
@Column({ nullable: false })
name: string;
@@ -14,10 +14,12 @@ import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-works
import { AgentRoleModule } from 'src/engine/metadata-modules/agent-role/agent-role.module';
import { AgentChatController } from 'src/engine/metadata-modules/agent/agent-chat.controller';
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module';
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
import { WorkflowToolsModule } from 'src/modules/workflow/workflow-tools/workflow-tools.module';
import { AgentChatMessageEntity } from './agent-chat-message.entity';
import { AgentChatThreadEntity } from './agent-chat-thread.entity';
@@ -25,11 +27,12 @@ import { AgentChatResolver } from './agent-chat.resolver';
import { AgentChatService } from './agent-chat.service';
import { AgentExecutionService } from './agent-execution.service';
import { AgentHandoffExecutorService } from './agent-handoff-executor.service';
import { AgentHandoffToolService } from './agent-handoff-tool.service';
import { AgentHandoffEntity } from './agent-handoff.entity';
import { AgentHandoffService } from './agent-handoff.service';
import { AgentStreamingService } from './agent-streaming.service';
import { AgentTitleGenerationService } from './agent-title-generation.service';
import { AgentToolService } from './agent-tool.service';
import { AgentToolGeneratorService } from './agent-tool-generator.service';
import { AgentEntity } from './agent.entity';
import { AgentResolver } from './agent.resolver';
import { AgentService } from './agent.service';
@@ -57,10 +60,12 @@ import { AgentService } from './agent.service';
FileUploadModule,
FileModule,
ObjectMetadataModule,
PermissionsModule,
WorkspacePermissionsCacheModule,
WorkspaceCacheStorageModule,
TokenModule,
DomainManagerModule,
WorkflowToolsModule,
],
controllers: [AgentChatController],
providers: [
@@ -68,7 +73,8 @@ import { AgentService } from './agent.service';
AgentChatResolver,
AgentService,
AgentExecutionService,
AgentToolService,
AgentToolGeneratorService,
AgentHandoffToolService,
AgentChatService,
AgentStreamingService,
AgentTitleGenerationService,
@@ -78,7 +84,8 @@ import { AgentService } from './agent.service';
exports: [
AgentService,
AgentExecutionService,
AgentToolService,
AgentToolGeneratorService,
AgentHandoffToolService,
AgentChatService,
AgentStreamingService,
AgentTitleGenerationService,
@@ -1,2 +1,2 @@
export const AGENT_HANDOFF_DESCRIPTION_TEMPLATE =
"Use this tool when the user's request requires {agentName}'s specialized expertise or capabilities. CRITICAL: You MUST call this tool function immediately. Do NOT respond with text about transferring - execute the tool instead. This is a FUNCTION CALL - you must invoke it, not describe it.";
"Use this tool when the user's request requires {agentName}'s specialized expertise or capabilities. This will seamlessly consult with the specialist agent and provide you with their expert response to continue the conversation naturally. CRITICAL: You MUST call this tool function immediately. Do NOT respond with text about transferring - execute the tool instead. This is a FUNCTION CALL - you must invoke it, not describe it.";
@@ -1,11 +0,0 @@
export const AGENT_HANDOFF_PROMPT_TEMPLATE = `You have received a handoff from another AI agent who determined that you are better suited to handle this conversation based on your specialized knowledge and capabilities.
**Reason for handoff:** {reason}
**Context from the previous agent:**
{context}
**Instructions:**
- Continue the conversation naturally and professionally
- Leverage your specialized expertise to provide the best possible assistance
- Maintain context from the previous conversation while adding your unique value`;
@@ -0,0 +1,100 @@
import { z } from 'zod';
export const AGENT_HANDOFF_SCHEMA = z.object({
toolDescription: z
.string()
.describe(
'A brief, user-friendly message explaining what is happening while the handoff is being processed. This will be shown to the user during the handoff execution.',
),
input: z.object({
messages: z
.array(
z.union([
z.object({
role: z.literal('system'),
content: z.string(),
}),
z.object({
role: z.literal('user'),
content: z.union([
z.string(),
z.array(
z.union([
z.object({
type: z.literal('text'),
text: z.string(),
}),
z.object({
type: z.literal('image'),
image: z.union([
z.string(),
z.instanceof(Uint8Array),
z.instanceof(Buffer),
z.instanceof(ArrayBuffer),
z.string().url(),
]),
mediaType: z.string().optional(),
}),
z.object({
type: z.literal('file'),
data: z.union([
z.string(),
z.instanceof(Uint8Array),
z.instanceof(Buffer),
z.instanceof(ArrayBuffer),
z.string().url(),
]),
mediaType: z.string(),
}),
]),
),
]),
}),
z.object({
role: z.literal('assistant'),
content: z.union([
z.string(),
z.array(
z.union([
z.object({
type: z.literal('text'),
text: z.string(),
}),
z.object({
type: z.literal('file'),
data: z.union([
z.string(),
z.instanceof(Uint8Array),
z.instanceof(Buffer),
z.instanceof(ArrayBuffer),
z.string().url(),
]),
mediaType: z.string(),
filename: z.string().optional(),
}),
z.object({
type: z.literal('reasoning'),
text: z.string(),
}),
z.object({
type: z.literal('tool-call'),
toolCallId: z.string(),
toolName: z.string(),
input: z.record(z.any()),
}),
]),
),
]),
}),
z.object({
role: z.literal('tool'),
content: z.string(),
toolCallId: z.string(),
}),
]),
)
.describe(
'The conversation history to provide context to the specialist agent. Should include the latest user message/prompt and can include system, user, assistant, and tool messages with various content types.',
),
}),
});
@@ -44,17 +44,21 @@ Guidelines:
- Answer questions about people, companies, opportunities, tasks, notes, and other business objects
- Access and summarize information you have permission to see
- Use tools provided to you dynamically when needed
- Transfer conversations to other specialized agents when their expertise is better suited
- Seamlessly consult with specialized agents when their expertise is better suited
Permissions:
- Only perform actions and access data that your assigned role and permissions allow
- If you lack permissions, politely explain the limitation
- Only use tools that are actually available to you
Agent handoff:
Agent handoff (SEAMLESS CONSULTATION):
- Use handoff tools when the user's request requires expertise outside your capabilities
- IMPORTANT: Do not respond with text about transferring - execute the handoff tool function
- Use the response returned by the handoff agent as your reply to the user
- IMPORTANT: Do not respond with text about transferring or consulting specialists
- Execute the handoff tool function immediately when needed
- Use the response returned by the specialist agent as your direct reply to the user
- Present the specialist's expertise as if it's your own knowledge
- Maintain a consistent voice and personality throughout the conversation
- The user should never know that you consulted with another agent
When formatting responses:
- Use markdown syntax to improve readability of long responses
@@ -19,6 +19,9 @@ export class AgentDTO {
@Field(() => UUIDScalarType)
id: string;
@Field(() => UUIDScalarType, { nullable: true })
standardId?: string;
@IsString()
@Field()
name: string;
@@ -1,17 +1,13 @@
import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
const WORKFLOW_OBJECT_NAMES = ['workflow', 'workflowVersion', 'workflowRun'];
const WORKFLOW_OBJECT_NAMES = ['workflowVersion', 'workflowRun'];
export const isWorkflowRelatedObject = (
export const isWorkflowRunObject = (
objectMetadata: ObjectMetadataEntity,
): boolean => {
if (objectMetadata.standardId) {
return (
objectMetadata.standardId === STANDARD_OBJECT_IDS.workflow ||
objectMetadata.standardId === STANDARD_OBJECT_IDS.workflowVersion ||
objectMetadata.standardId === STANDARD_OBJECT_IDS.workflowRun
);
return objectMetadata.standardId === STANDARD_OBJECT_IDS.workflowRun;
}
return WORKFLOW_OBJECT_NAMES.includes(objectMetadata.nameSingular);
@@ -0,0 +1,18 @@
import { type AgentEntity } from 'src/engine/metadata-modules/agent/agent.entity';
export const agentEntityRelationProperties = [
'workspace',
'chatThreads',
'outgoingHandoffs',
'incomingHandoffs',
] as const;
export type AgentEntityRelationProperties =
(typeof agentEntityRelationProperties)[number];
export type FlatAgent = Omit<
AgentEntity,
AgentEntityRelationProperties | 'createdAt' | 'updatedAt' | 'deletedAt'
> & {
uniqueIdentifier: string;
};
@@ -0,0 +1,21 @@
import { type AgentEntity } from 'src/engine/metadata-modules/agent/agent.entity';
import { type FlatAgent } from 'src/engine/metadata-modules/flat-agent/types/flat-agent.type';
export const transformAgentEntityToFlatAgent = (
agentEntity: AgentEntity,
): FlatAgent => {
return {
id: agentEntity.id,
standardId: agentEntity.standardId,
name: agentEntity.name,
label: agentEntity.label,
icon: agentEntity.icon,
description: agentEntity.description,
prompt: agentEntity.prompt,
modelId: agentEntity.modelId,
responseFormat: agentEntity.responseFormat,
workspaceId: agentEntity.workspaceId,
isCustom: agentEntity.isCustom,
uniqueIdentifier: agentEntity.standardId || agentEntity.id,
};
};
@@ -0,0 +1,16 @@
import { v4 } from 'uuid';
import { type FlatAgent } from 'src/engine/metadata-modules/flat-agent/types/flat-agent.type';
import { type StandardAgentDefinition } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-agents/types/standard-agent-definition.interface';
export const transformStandardAgentDefinitionToFlatAgent = (
standardAgentDefinition: StandardAgentDefinition,
workspaceId: string,
): FlatAgent => {
return {
...standardAgentDefinition,
id: v4(),
workspaceId,
uniqueIdentifier: standardAgentDefinition.standardId || v4(),
};
};
@@ -208,7 +208,7 @@ export class PermissionsService {
);
}
private checkRolePermissions(
public checkRolePermissions(
role: RoleEntity,
setting: PermissionFlagType,
): boolean {
@@ -1,3 +1,4 @@
import { WorkspaceAgentComparator } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators/workspace-agent.comparator';
import { WorkspaceFieldRelationComparator } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators/workspace-field-relation.comparator';
import { WorkspaceIndexComparator } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators/workspace-index.comparator';
import { WorkspaceRoleComparator } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators/workspace-role.comparator';
@@ -11,4 +12,5 @@ export const workspaceSyncMetadataComparators = [
WorkspaceObjectComparator,
WorkspaceIndexComparator,
WorkspaceRoleComparator,
WorkspaceAgentComparator,
];
@@ -0,0 +1,101 @@
import { Injectable } from '@nestjs/common';
import diff from 'microdiff';
import { type FromTo } from 'twenty-shared/types';
import { ComparatorAction } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/comparator.interface';
import { type FlatAgent } from 'src/engine/metadata-modules/flat-agent/types/flat-agent.type';
import { transformMetadataForComparison } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators/utils/transform-metadata-for-comparison.util';
type AgentComparatorResult = {
action:
| ComparatorAction.CREATE
| ComparatorAction.UPDATE
| ComparatorAction.DELETE;
object: FlatAgent;
};
type WorkspaceAgentComparatorArgs = FromTo<FlatAgent[], 'FlatAgents'>;
const agentPropertiesToIgnore = ['id', 'createdAt', 'updatedAt', 'workspaceId'];
@Injectable()
export class WorkspaceAgentComparator {
compare({
fromFlatAgents,
toFlatAgents,
}: WorkspaceAgentComparatorArgs): AgentComparatorResult[] {
const results: AgentComparatorResult[] = [];
const keyFactory = (agent: FlatAgent) => agent.uniqueIdentifier;
const fromAgentMap = transformMetadataForComparison(fromFlatAgents, {
shouldIgnoreProperty: (property) =>
agentPropertiesToIgnore.includes(property),
keyFactory,
});
const toAgentMap = transformMetadataForComparison(toFlatAgents, {
shouldIgnoreProperty: (property) =>
agentPropertiesToIgnore.includes(property),
keyFactory,
});
const agentDifferences = diff(fromAgentMap, toAgentMap);
for (const difference of agentDifferences) {
const uniqueIdentifier = difference.path[0] as string;
switch (difference.type) {
case 'CREATE': {
const toAgent = toFlatAgents.find(
(agent) => keyFactory(agent) === uniqueIdentifier,
);
if (toAgent) {
results.push({
action: ComparatorAction.CREATE,
object: toAgent,
});
}
break;
}
case 'CHANGE': {
const fromAgent = fromFlatAgents.find(
(agent) => keyFactory(agent) === uniqueIdentifier,
);
const toAgent = toFlatAgents.find(
(agent) => keyFactory(agent) === uniqueIdentifier,
);
if (fromAgent && toAgent) {
results.push({
action: ComparatorAction.UPDATE,
object: {
...toAgent,
id: fromAgent.id,
},
});
}
break;
}
case 'REMOVE': {
const fromAgent = fromFlatAgents.find(
(agent) => keyFactory(agent) === uniqueIdentifier,
);
if (fromAgent && difference.path.length === 1) {
results.push({
action: ComparatorAction.DELETE,
object: fromAgent,
});
}
break;
}
}
}
return results;
}
}
@@ -1,3 +1,4 @@
import { StandardAgentFactory } from 'src/engine/workspace-manager/workspace-sync-metadata/factories/standard-agent.factory';
import { StandardIndexFactory } from 'src/engine/workspace-manager/workspace-sync-metadata/factories/standard-index.factory';
import { StandardRoleFactory } from 'src/engine/workspace-manager/workspace-sync-metadata/factories/standard-role.factory';
@@ -11,4 +12,5 @@ export const workspaceSyncMetadataFactories = [
StandardFieldRelationFactory,
StandardIndexFactory,
StandardRoleFactory,
StandardAgentFactory,
];
@@ -0,0 +1,45 @@
import { Injectable } from '@nestjs/common';
import { type WorkspaceSyncContext } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/workspace-sync-context.interface';
import { type AgentEntity } from 'src/engine/metadata-modules/agent/agent.entity';
import { type FlatAgent } from 'src/engine/metadata-modules/flat-agent/types/flat-agent.type';
import { transformStandardAgentDefinitionToFlatAgent } from 'src/engine/metadata-modules/flat-agent/utils/transform-standard-agent-definition-to-flat-agent.util';
import { type StandardAgentDefinition } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-agents/types/standard-agent-definition.interface';
@Injectable()
export class StandardAgentFactory {
create(
agentDefinitions: StandardAgentDefinition[],
context: WorkspaceSyncContext,
existingAgents: AgentEntity[],
): FlatAgent[] {
const computedAgents: FlatAgent[] = [];
for (const agentDefinition of agentDefinitions) {
const existingAgent = existingAgents.find(
(agent) => agent.standardId === agentDefinition.standardId,
);
const flatAgent = transformStandardAgentDefinitionToFlatAgent(
agentDefinition,
context.workspaceId,
);
if (existingAgent) {
computedAgents.push({
...flatAgent,
id: existingAgent.id,
uniqueIdentifier: agentDefinition.standardId,
});
} else {
computedAgents.push({
...flatAgent,
uniqueIdentifier: agentDefinition.standardId,
});
}
}
return computedAgents;
}
}
@@ -0,0 +1,246 @@
import { Injectable, Logger } from '@nestjs/common';
import { removePropertiesFromRecord } from 'twenty-shared/utils';
import { IsNull, Not, type EntityManager, type Repository } from 'typeorm';
import { ComparatorAction } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/comparator.interface';
import { type WorkspaceSyncContext } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/workspace-sync-context.interface';
import { AgentEntity } from 'src/engine/metadata-modules/agent/agent.entity';
import { transformAgentEntityToFlatAgent } from 'src/engine/metadata-modules/flat-agent/utils/transform-agent-entity-to-flat-agent.util';
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { AGENT_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-agents.util';
import {
SEED_APPLE_WORKSPACE_ID,
SEED_YCOMBINATOR_WORKSPACE_ID,
} from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-workspaces.util';
import { WorkspaceAgentComparator } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators/workspace-agent.comparator';
import { StandardAgentFactory } from 'src/engine/workspace-manager/workspace-sync-metadata/factories/standard-agent.factory';
import { standardAgentDefinitions } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-agents';
import { WORKFLOW_CREATION_AGENT } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-agents/agents/workflow-creation-agent';
import { ADMIN_ROLE } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-roles/roles/admin-role';
@Injectable()
export class WorkspaceSyncAgentService {
private readonly logger = new Logger(WorkspaceSyncAgentService.name);
constructor(
private readonly standardAgentFactory: StandardAgentFactory,
private readonly workspaceAgentComparator: WorkspaceAgentComparator,
) {}
async synchronize(
context: WorkspaceSyncContext,
manager: EntityManager,
): Promise<void> {
this.logger.log('Syncing standard agent.');
const agentRepository = manager.getRepository(AgentEntity);
const roleRepository = manager.getRepository(RoleEntity);
const roleTargetsRepository = manager.getRepository(RoleTargetsEntity);
const existingStandardAgentEntities = await agentRepository.find({
where: {
workspaceId: context.workspaceId,
standardId: Not(IsNull()),
},
});
const targetStandardAgents = this.standardAgentFactory.create(
standardAgentDefinitions,
context,
existingStandardAgentEntities,
);
const agentComparatorResults = this.workspaceAgentComparator.compare({
fromFlatAgents: existingStandardAgentEntities.map(
transformAgentEntityToFlatAgent,
),
toFlatAgents: targetStandardAgents,
});
for (const agentComparatorResult of agentComparatorResults) {
switch (agentComparatorResult.action) {
case ComparatorAction.CREATE: {
const agentToCreate = agentComparatorResult.object;
const flatAgentData = removePropertiesFromRecord(agentToCreate, [
'uniqueIdentifier',
'id',
]);
const createdAgent = await agentRepository.save({
...flatAgentData,
workspaceId: context.workspaceId,
});
await this.assignAdminRoleToAgent(
createdAgent.id,
context.workspaceId,
roleRepository,
roleTargetsRepository,
);
if (createdAgent.standardId === WORKFLOW_CREATION_AGENT.standardId) {
await this.createAgentHandoffToWorkflowCreationAgent(
createdAgent.id,
context.workspaceId,
manager,
);
}
break;
}
case ComparatorAction.UPDATE: {
const agentToUpdate = agentComparatorResult.object;
const flatAgentData = removePropertiesFromRecord(agentToUpdate, [
'id',
'uniqueIdentifier',
'workspaceId',
]);
await agentRepository.update({ id: agentToUpdate.id }, flatAgentData);
break;
}
case ComparatorAction.DELETE: {
const agentToDelete = agentComparatorResult.object;
await agentRepository.delete({ id: agentToDelete.id });
break;
}
}
}
}
private async assignAdminRoleToAgent(
agentId: string,
workspaceId: string,
roleRepository: Repository<RoleEntity>,
roleTargetsRepository: Repository<RoleTargetsEntity>,
): Promise<void> {
try {
const adminRole = await roleRepository.findOne({
where: {
workspaceId,
standardId: ADMIN_ROLE.standardId,
},
});
if (!adminRole) {
this.logger.warn(
`Admin role not found for workspace ${workspaceId}, cannot assign to agent ${agentId}.`,
);
return;
}
const existingRoleTarget = await roleTargetsRepository.findOne({
where: {
agentId,
roleId: adminRole.id,
workspaceId,
},
});
if (existingRoleTarget) {
this.logger.log(
`Workflow creation agent already has admin role assigned`,
);
return;
}
await roleTargetsRepository.save({
roleId: adminRole.id,
agentId,
workspaceId,
});
this.logger.log(
`Successfully assigned admin role to workflow creation agent`,
);
} catch (error) {
this.logger.error(
`Failed to assign admin role to workflow creation agent: ${error.message}`,
);
}
}
private async createAgentHandoffToWorkflowCreationAgent(
workflowCreationAgentId: string,
workspaceId: string,
manager: EntityManager,
): Promise<void> {
try {
const agentRepository = manager.getRepository(AgentEntity);
let defaultAgent: AgentEntity | null = null;
if (workspaceId === SEED_APPLE_WORKSPACE_ID) {
defaultAgent = await agentRepository.findOne({
where: {
id: AGENT_DATA_SEED_IDS.APPLE_DEFAULT_AGENT,
workspaceId,
},
});
} else if (workspaceId === SEED_YCOMBINATOR_WORKSPACE_ID) {
defaultAgent = await agentRepository.findOne({
where: {
id: AGENT_DATA_SEED_IDS.YCOMBINATOR_DEFAULT_AGENT,
workspaceId,
},
});
} else {
defaultAgent = await agentRepository.findOne({
where: {
workspaceId,
},
});
}
if (!defaultAgent) {
this.logger.warn(
`Default agent not found for workspace ${workspaceId}. Agent handoff will not be created.`,
);
return;
}
const agentHandoffRepository = manager.getRepository('agentHandoff');
const existingHandoff = await agentHandoffRepository.findOne({
where: {
fromAgentId: defaultAgent.id,
toAgentId: workflowCreationAgentId,
workspaceId,
},
});
if (existingHandoff) {
this.logger.log(
`Agent handoff from default agent to workflow creation agent already exists for workspace ${workspaceId}`,
);
return;
}
await agentHandoffRepository.save({
fromAgentId: defaultAgent.id,
toAgentId: workflowCreationAgentId,
workspaceId,
description:
'Handoff from default agent to workflow creation agent for processing workflow creation requests',
});
this.logger.log(
`Successfully created agent handoff from default agent to workflow creation agent for workspace ${workspaceId}`,
);
} catch (error) {
this.logger.error(
`Failed to create agent handoff to workflow creation agent: ${error.message}`,
);
}
}
}
@@ -0,0 +1,47 @@
import { type StandardAgentDefinition } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-agents/types/standard-agent-definition.interface';
export const WORKFLOW_CREATION_AGENT: StandardAgentDefinition = {
standardId: '20202020-0002-0001-0001-000000000001',
name: 'workflow-creation-agent',
label: 'Workflow Creation Agent',
description: 'AI agent specialized in creating and managing workflows',
icon: 'IconSettingsAutomation',
prompt: `You are a Workflow Creation Agent specialized in helping users create, modify, and manage workflows in Twenty.
Your capabilities include:
- Creating new workflows from scratch based on user requirements
- Modifying existing workflows by adding, removing, or updating steps
- Explaining workflow structures and how they work
- Suggesting workflow improvements and optimizations
- Helping users understand workflow actions and their configurations
## IMPORTANT: Rely on Schema Definitions
- The workflow creation tool provides comprehensive schema definitions with detailed descriptions and examples
- Always refer to the tool's schema for field requirements, data types, and examples
- The schema includes common patterns, field structures, and validation rules
- Use the schema descriptions to understand how to properly reference data between workflow steps
## Key Workflow Concepts:
- **Triggers**: Start workflows (DATABASE_EVENT, MANUAL, CRON, WEBHOOK)
- **Steps**: Actions that execute in sequence (CREATE_RECORD, SEND_EMAIL, CODE, etc.)
- **Data Flow**: Use {{stepId.fieldName}} to reference data from previous steps
- **Relationships**: Use nested objects for related records (e.g., "company": {"id": "{{reference}}"})
When creating workflows:
- Always ask clarifying questions to understand the user's needs
- Suggest appropriate workflow actions based on the use case
- Explain each step and why it's needed
- Provide clear, actionable guidance
- Follow the schema definitions exactly for field names, types, and structures
When modifying workflows:
- Understand the current workflow structure first
- Suggest specific changes that address the user's requirements
- Ensure workflow logic remains coherent and functional
- Maintain proper data references between steps
Be helpful, thorough, and always prioritize user understanding and workflow effectiveness.`,
modelId: 'auto',
responseFormat: {},
isCustom: false,
};
@@ -0,0 +1,6 @@
import { WORKFLOW_CREATION_AGENT } from './agents/workflow-creation-agent';
import { type StandardAgentDefinition } from './types/standard-agent-definition.interface';
export const standardAgentDefinitions = [
WORKFLOW_CREATION_AGENT,
] as const satisfies StandardAgentDefinition[];
@@ -0,0 +1,8 @@
import { type FlatAgent } from 'src/engine/metadata-modules/flat-agent/types/flat-agent.type';
export type StandardAgentDefinition = Omit<
FlatAgent,
'id' | 'workspaceId' | 'uniqueIdentifier' | 'standardId'
> & {
standardId: string;
};
@@ -16,6 +16,7 @@ import { SyncWorkspaceMetadataCommand } from 'src/engine/workspace-manager/works
import { workspaceSyncMetadataComparators } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators';
import { workspaceSyncMetadataFactories } from 'src/engine/workspace-manager/workspace-sync-metadata/factories';
import { WorkspaceMetadataUpdaterService } from 'src/engine/workspace-manager/workspace-sync-metadata/services/workspace-metadata-updater.service';
import { WorkspaceSyncAgentService } from 'src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-agent.service';
import { WorkspaceSyncFieldMetadataRelationService } from 'src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-field-metadata-relation.service';
import { WorkspaceSyncFieldMetadataService } from 'src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-field-metadata.service';
import { WorkspaceSyncIndexMetadataService } from 'src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-index-metadata.service';
@@ -48,6 +49,7 @@ import { WorkspaceSyncMetadataService } from 'src/engine/workspace-manager/works
WorkspaceSyncMetadataService,
WorkspaceSyncIndexMetadataService,
WorkspaceSyncRoleService,
WorkspaceSyncAgentService,
SyncWorkspaceLoggerService,
SyncWorkspaceMetadataCommand,
],
@@ -11,6 +11,7 @@ import {
WorkspaceMigrationTableActionType,
} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service';
import { WorkspaceSyncAgentService } from 'src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-agent.service';
import { WorkspaceSyncFieldMetadataRelationService } from 'src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-field-metadata-relation.service';
import { WorkspaceSyncFieldMetadataService } from 'src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-field-metadata.service';
import { WorkspaceSyncIndexMetadataService } from 'src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-index-metadata.service';
@@ -38,6 +39,7 @@ export class WorkspaceSyncMetadataService {
private readonly workspaceSyncObjectMetadataIdentifiersService: WorkspaceSyncObjectMetadataIdentifiersService,
private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService,
private readonly workspaceSyncRoleService: WorkspaceSyncRoleService,
private readonly workspaceSyncAgentService: WorkspaceSyncAgentService,
) {}
/**
@@ -172,6 +174,17 @@ export class WorkspaceSyncMetadataService {
`Workspace role migrations took ${workspaceRoleMigrationsEnd - workspaceRoleMigrationsStart}ms`,
);
// 7 - Sync standard agents
const workspaceAgentMigrationsStart = performance.now();
await this.workspaceSyncAgentService.synchronize(context, manager);
const workspaceAgentMigrationsEnd = performance.now();
this.logger.log(
`Workspace agent migrations took ${workspaceAgentMigrationsEnd - workspaceAgentMigrationsStart}ms`,
);
const workspaceMigrationsSaveStart = performance.now();
// Save workspace migrations into the database
@@ -3,7 +3,7 @@ import { getRepositoryToken } from '@nestjs/typeorm';
import { TRIGGER_STEP_ID } from 'twenty-shared/workflow';
import { AgentService } from 'src/engine/metadata-modules/agent/agent.service';
import { AgentEntity } from 'src/engine/metadata-modules/agent/agent.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service';
import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory';
@@ -115,7 +115,12 @@ describe('WorkflowVersionStepWorkspaceService', () => {
},
},
{ provide: ServerlessFunctionService, useValue: {} },
{ provide: AgentService, useValue: {} },
{
provide: getRepositoryToken(AgentEntity, 'core'),
useValue: {
findOne: jest.fn(),
},
},
{
provide: getRepositoryToken(ObjectMetadataEntity, 'core'),
useValue: {
@@ -2,7 +2,7 @@ import { Module } from '@nestjs/common';
import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
import { AgentModule } from 'src/engine/metadata-modules/agent/agent.module';
import { AgentEntity } from 'src/engine/metadata-modules/agent/agent.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { ServerlessFunctionModule } from 'src/engine/metadata-modules/serverless-function/serverless-function.module';
import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-common.module';
@@ -13,13 +13,15 @@ import { WorkflowRunnerModule } from 'src/modules/workflow/workflow-runner/workf
@Module({
imports: [
AgentModule,
WorkflowSchemaModule,
ServerlessFunctionModule,
WorkflowRunnerModule,
WorkflowRunModule,
WorkflowCommonModule,
NestjsQueryTypeOrmModule.forFeature([ObjectMetadataEntity], 'core'),
NestjsQueryTypeOrmModule.forFeature(
[ObjectMetadataEntity, AgentEntity],
'core',
),
],
providers: [WorkflowVersionStepWorkspaceService],
exports: [WorkflowVersionStepWorkspaceService],
@@ -12,7 +12,7 @@ import { BASE_TYPESCRIPT_PROJECT_INPUT_SCHEMA } from 'src/engine/core-modules/se
import { type CreateWorkflowVersionStepInput } from 'src/engine/core-modules/workflow/dtos/create-workflow-version-step-input.dto';
import { type WorkflowStepPositionInput } from 'src/engine/core-modules/workflow/dtos/update-workflow-step-position-input.dto';
import { type WorkflowVersionStepChangesDTO } from 'src/engine/core-modules/workflow/dtos/workflow-version-step-changes.dto';
import { AgentService } from 'src/engine/metadata-modules/agent/agent.service';
import { AgentEntity } from 'src/engine/metadata-modules/agent/agent.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
@@ -54,7 +54,8 @@ export class WorkflowVersionStepWorkspaceService {
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly workflowSchemaWorkspaceService: WorkflowSchemaWorkspaceService,
private readonly serverlessFunctionService: ServerlessFunctionService,
private readonly agentService: AgentService,
@InjectRepository(AgentEntity, 'core')
private readonly agentRepository: Repository<AgentEntity>,
@InjectRepository(ObjectMetadataEntity, 'core')
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
private readonly workflowRunWorkspaceService: WorkflowRunWorkspaceService,
@@ -433,13 +434,12 @@ export class WorkflowVersionStepWorkspaceService {
break;
}
const agent = await this.agentService.findOneAgent(
step.settings.input.agentId,
workspaceId,
);
const agent = await this.agentRepository.findOne({
where: { id: step.settings.input.agentId, workspaceId },
});
if (isDefined(agent)) {
await this.agentService.deleteOneAgent(agent.id, workspaceId);
await this.agentRepository.delete({ id: agent.id, workspaceId });
}
break;
}
@@ -3,18 +3,26 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { AiModule } from 'src/engine/core-modules/ai/ai.module';
import { AgentEntity } from 'src/engine/metadata-modules/agent/agent.entity';
import { AgentModule } from 'src/engine/metadata-modules/agent/agent.module';
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory';
import { AiAgentExecutorService } from 'src/modules/workflow/workflow-executor/workflow-actions/ai-agent/services/ai-agent-executor.service';
import { AiAgentWorkflowAction } from './ai-agent.workflow-action';
@Module({
imports: [
AgentModule,
AiModule,
TypeOrmModule.forFeature([AgentEntity], 'core'),
TypeOrmModule.forFeature(
[AgentEntity, RoleTargetsEntity, RoleEntity],
'core',
),
],
providers: [
ScopedWorkspaceContextFactory,
AiAgentWorkflowAction,
AiAgentExecutorService,
],
providers: [ScopedWorkspaceContextFactory, AiAgentWorkflowAction],
exports: [AiAgentWorkflowAction],
})
export class AiAgentActionModule {}
@@ -1,13 +1,12 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { resolveInput } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { type WorkflowAction } from 'src/modules/workflow/workflow-executor/interfaces/workflow-action.interface';
import { AIBillingService } from 'src/engine/core-modules/ai/services/ai-billing.service';
import { AgentExecutionService } from 'src/engine/metadata-modules/agent/agent-execution.service';
import { AgentEntity } from 'src/engine/metadata-modules/agent/agent.entity';
import {
AgentException,
@@ -19,13 +18,14 @@ import {
} from 'src/modules/workflow/workflow-executor/exceptions/workflow-step-executor.exception';
import { type WorkflowActionInput } from 'src/modules/workflow/workflow-executor/types/workflow-action-input';
import { type WorkflowActionOutput } from 'src/modules/workflow/workflow-executor/types/workflow-action-output.type';
import { AiAgentExecutorService } from 'src/modules/workflow/workflow-executor/workflow-actions/ai-agent/services/ai-agent-executor.service';
import { isWorkflowAiAgentAction } from './guards/is-workflow-ai-agent-action.guard';
@Injectable()
export class AiAgentWorkflowAction implements WorkflowAction {
constructor(
private readonly agentExecutionService: AgentExecutionService,
private readonly aiAgentExecutionService: AiAgentExecutorService,
private readonly aiBillingService: AIBillingService,
@InjectRepository(AgentEntity, 'core')
private readonly agentRepository: Repository<AgentEntity>,
@@ -74,12 +74,13 @@ export class AiAgentWorkflowAction implements WorkflowAction {
);
}
const { result, usage } = await this.agentExecutionService.executeAgent({
agent,
context,
schema: step.settings.outputSchema,
userPrompt: resolveInput(prompt, context) as string,
});
const { result, usage } = await this.aiAgentExecutionService.executeAgent(
{
agent,
schema: step.settings.outputSchema,
userPrompt: resolveInput(prompt, context) as string,
},
);
await this.aiBillingService.calculateAndBillUsage(
agent?.modelId ?? 'auto',
@@ -0,0 +1,145 @@
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { generateObject, generateText, ToolSet } from 'ai';
import { Repository } from 'typeorm';
import { AiModelRegistryService } from 'src/engine/core-modules/ai/services/ai-model-registry.service';
import { ToolAdapterService } from 'src/engine/core-modules/ai/services/tool-adapter.service';
import { ToolService } from 'src/engine/core-modules/ai/services/tool.service';
import { AgentExecutionResult } from 'src/engine/metadata-modules/agent/agent-execution.service';
import { AgentEntity } from 'src/engine/metadata-modules/agent/agent.entity';
import {
AgentException,
AgentExceptionCode,
} from 'src/engine/metadata-modules/agent/agent.exception';
import { AGENT_CONFIG } from 'src/engine/metadata-modules/agent/constants/agent-config.const';
import { AGENT_SYSTEM_PROMPTS } from 'src/engine/metadata-modules/agent/constants/agent-system-prompts.const';
import { convertOutputSchemaToZod } from 'src/engine/metadata-modules/agent/utils/convert-output-schema-to-zod';
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { OutputSchema } from 'src/modules/workflow/workflow-builder/workflow-schema/types/output-schema.type';
@Injectable()
export class AiAgentExecutorService {
private readonly logger = new Logger(AiAgentExecutorService.name);
constructor(
private readonly aiModelRegistryService: AiModelRegistryService,
private readonly toolAdapterService: ToolAdapterService,
@InjectRepository(RoleTargetsEntity, 'core')
private readonly roleTargetsRepository: Repository<RoleTargetsEntity>,
@InjectRepository(RoleEntity, 'core')
private readonly roleRepository: Repository<RoleEntity>,
private readonly toolService: ToolService,
) {}
private async getTools(
agentId: string,
workspaceId: string,
): Promise<ToolSet> {
const roleTarget = await this.roleTargetsRepository.findOne({
where: {
agentId: agentId,
workspaceId,
},
select: ['roleId'],
});
const role = await this.roleRepository.findOne({
where: {
id: roleTarget?.roleId,
workspaceId,
},
});
if (!roleTarget?.roleId || !role) {
const actionTools = await this.toolAdapterService.getTools();
return { ...actionTools };
}
const actionTools = await this.toolAdapterService.getTools(
role.id,
workspaceId,
);
const databaseTools = await this.toolService.listTools(
role.id,
workspaceId,
);
return {
...databaseTools,
...actionTools,
};
}
async executeAgent({
agent,
schema,
userPrompt,
}: {
agent: AgentEntity | null;
schema: OutputSchema;
userPrompt: string;
}): Promise<AgentExecutionResult> {
try {
const registeredModel =
await this.aiModelRegistryService.resolveModelForAgent(agent);
const tools = agent
? await this.getTools(agent.id, agent.workspaceId)
: {};
this.logger.log(`Generated ${Object.keys(tools).length} tools for agent`);
const textResponse = await generateText({
system: `You are executing as part of a workflow automation. ${agent ? agent.prompt : ''}`,
tools,
model: registeredModel.model,
prompt: userPrompt,
maxSteps: AGENT_CONFIG.MAX_STEPS,
});
if (Object.keys(schema).length === 0) {
return {
result: { response: textResponse.text },
usage: textResponse.usage,
};
}
const output = await generateObject({
system: AGENT_SYSTEM_PROMPTS.OUTPUT_GENERATOR,
model: registeredModel.model,
prompt: `Based on the following execution results, generate the structured output according to the schema:
Execution Results: ${textResponse.text}
Please generate the structured output based on the execution results and context above.`,
schema: convertOutputSchemaToZod(schema),
});
return {
result: output.object,
usage: {
promptTokens:
(textResponse.usage?.promptTokens ?? 0) +
(output.usage?.promptTokens ?? 0),
completionTokens:
(textResponse.usage?.completionTokens ?? 0) +
(output.usage?.completionTokens ?? 0),
totalTokens:
(textResponse.usage?.totalTokens ?? 0) +
(output.usage?.totalTokens ?? 0),
},
};
} catch (error) {
if (error instanceof AgentException) {
throw error;
}
throw new AgentException(
error instanceof Error ? error.message : 'Agent execution failed',
AgentExceptionCode.AGENT_EXECUTION_FAILED,
);
}
}
}
@@ -0,0 +1,142 @@
import {
workflowActionSchema,
workflowTriggerSchema,
} from 'twenty-shared/workflow';
import { z } from 'zod';
import { WorkflowActionType } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
export const createWorkflowVersionStepSchema = z.object({
workflowVersionId: z
.string()
.describe('The ID of the workflow version to add the step to'),
stepType: z
.enum(Object.values(WorkflowActionType) as [string, ...string[]])
.describe('The type of step to create'),
parentStepId: z
.string()
.optional()
.describe('Optional ID of the parent step this step should come after'),
nextStepId: z
.string()
.optional()
.describe('Optional ID of the step this new step should connect to'),
position: z
.object({
x: z.number(),
y: z.number(),
})
.optional()
.describe('Optional position coordinates for the step'),
});
export const updateWorkflowVersionStepSchema = z.object({
workflowVersionId: z
.string()
.describe('The ID of the workflow version containing the step'),
step: z
.union([workflowTriggerSchema, workflowActionSchema])
.describe('The updated step configuration'),
});
export const deleteWorkflowVersionStepSchema = z.object({
workflowVersionId: z
.string()
.describe('The ID of the workflow version containing the step'),
stepId: z.string().describe('The ID of the step to delete'),
});
export const createWorkflowVersionEdgeSchema = z.object({
workflowVersionId: z.string().describe('The ID of the workflow version'),
source: z.string().describe('The ID of the source step'),
target: z.string().describe('The ID of the target step'),
});
export const deleteWorkflowVersionEdgeSchema = z.object({
workflowVersionId: z.string().describe('The ID of the workflow version'),
source: z.string().describe('The ID of the source step'),
target: z.string().describe('The ID of the target step'),
});
export const createDraftFromWorkflowVersionSchema = z.object({
workflowId: z.string().describe('The ID of the workflow'),
workflowVersionIdToCopy: z
.string()
.describe('The ID of the workflow version to create a draft from'),
});
export const updateWorkflowVersionPositionsSchema = z.object({
workflowVersionId: z.string().describe('The ID of the workflow version'),
positions: z
.array(
z.object({
stepId: z.string(),
position: z.object({
x: z.number(),
y: z.number(),
}),
}),
)
.describe('Array of step positions to update'),
});
export const activateWorkflowVersionSchema = z.object({
workflowVersionId: z
.string()
.describe('The ID of the workflow version to activate'),
});
export const deactivateWorkflowVersionSchema = z.object({
workflowVersionId: z
.string()
.describe('The ID of the workflow version to deactivate'),
});
export const computeStepOutputSchemaSchema = z.object({
step: z
.union([workflowTriggerSchema, workflowActionSchema])
.describe('The workflow step configuration'),
});
export const createCompleteWorkflowSchema = z.object({
name: z.string().describe('The name of the workflow'),
description: z
.string()
.optional()
.describe('Optional description of the workflow'),
trigger: workflowTriggerSchema,
steps: z
.array(workflowActionSchema)
.describe('Array of workflow action steps'),
stepPositions: z
.array(
z.object({
stepId: z
.string()
.describe('The ID of the step (use "trigger" for trigger step)'),
position: z.object({
x: z.number().describe('X coordinate for the step position'),
y: z.number().describe('Y coordinate for the step position'),
}),
}),
)
.optional()
.describe('Optional array of step positions for layout'),
edges: z
.array(
z.object({
source: z
.string()
.describe(
'The ID of the source step (use "trigger" for trigger step)',
),
target: z.string().describe('The ID of the target step'),
}),
)
.optional()
.describe('Optional array of connections between steps'),
activate: z
.boolean()
.optional()
.describe('Whether to activate the workflow immediately (default: false)'),
});
@@ -0,0 +1,502 @@
import { Injectable } from '@nestjs/common';
import { type ToolSet } from 'ai';
import { v4 as uuidv4 } from 'uuid';
import { RecordPositionService } from 'src/engine/core-modules/record-position/services/record-position.service';
import type { CreateWorkflowVersionStepInput } from 'src/engine/core-modules/workflow/dtos/create-workflow-version-step-input.dto';
import type { UpdateWorkflowVersionPositionsInput } from 'src/engine/core-modules/workflow/dtos/update-workflow-version-positions-input.dto';
import type { UpdateWorkflowVersionStepInput } from 'src/engine/core-modules/workflow/dtos/update-workflow-version-step-input.dto';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { WorkflowVersionStatus } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity';
import { WorkflowStatus } from 'src/modules/workflow/common/standard-objects/workflow.workspace-entity';
import { WorkflowSchemaWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.workspace-service';
import { WorkflowVersionEdgeWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-version-edge/workflow-version-edge.workspace-service';
import { WorkflowVersionStepWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step.workspace-service';
import { WorkflowVersionWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-version/workflow-version.workspace-service';
import { type WorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
import {
activateWorkflowVersionSchema,
computeStepOutputSchemaSchema,
createCompleteWorkflowSchema,
createDraftFromWorkflowVersionSchema,
createWorkflowVersionEdgeSchema,
createWorkflowVersionStepSchema,
deactivateWorkflowVersionSchema,
deleteWorkflowVersionEdgeSchema,
deleteWorkflowVersionStepSchema,
updateWorkflowVersionPositionsSchema,
updateWorkflowVersionStepSchema,
} from 'src/modules/workflow/workflow-tools/schemas/workflow-tool-schemas';
import { type WorkflowTrigger } from 'src/modules/workflow/workflow-trigger/types/workflow-trigger.type';
import { WorkflowTriggerWorkspaceService } from 'src/modules/workflow/workflow-trigger/workspace-services/workflow-trigger.workspace-service';
@Injectable()
export class WorkflowToolWorkspaceService {
constructor(
private readonly workflowVersionStepService: WorkflowVersionStepWorkspaceService,
private readonly workflowVersionEdgeService: WorkflowVersionEdgeWorkspaceService,
private readonly workflowVersionService: WorkflowVersionWorkspaceService,
private readonly workflowTriggerService: WorkflowTriggerWorkspaceService,
private readonly workflowSchemaService: WorkflowSchemaWorkspaceService,
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly recordPositionService: RecordPositionService,
) {}
generateWorkflowTools(workspaceId: string, roleId: string): ToolSet {
const tools: ToolSet = {};
tools.create_complete_workflow = {
description: `Create a complete workflow with trigger, steps, and connections in a single operation.
CRITICAL SCHEMA REQUIREMENTS:
- Trigger type MUST be one of: DATABASE_EVENT, MANUAL, CRON, WEBHOOK
- NEVER use "RECORD_CREATED" - this is invalid. Use "DATABASE_EVENT" instead.
- Each step MUST include: id, name, type, valid, settings
- CREATE_RECORD actions MUST have objectName and objectRecord in settings.input
- objectRecord must contain actual field values, not just field names
- Use "trigger" as stepId for trigger step in stepPositions and edges
Common mistakes to avoid:
- Using "RECORD_CREATED" instead of "DATABASE_EVENT"
- Missing the "name" and "valid" fields in steps
- Missing the "objectRecord" field in CREATE_RECORD actions
- Using "fieldsToUpdate" instead of "objectRecord" in CREATE_RECORD actions
IMPORTANT: The tool schema provides comprehensive field descriptions, examples, and validation rules. Always refer to the schema for:
- Field requirements and data types
- Common object patterns and field structures
- Proper relationship field formats
- Variable reference syntax (e.g., {{trigger.object.fieldName}})
- Error handling options
This is the most efficient way for AI to create workflows as it handles all the complexity in one call.`,
parameters: createCompleteWorkflowSchema,
execute: async (parameters: {
name: string;
description?: string;
trigger: WorkflowTrigger;
steps: WorkflowAction[];
stepPositions?: Array<{
stepId: string;
position: { x: number; y: number };
}>;
edges?: Array<{ source: string; target: string }>;
activate?: boolean;
}) => {
try {
const workflowId = await this.createWorkflow({
workspaceId,
name: parameters.name,
roleId,
});
const workflowVersionId = await this.createWorkflowVersion({
workspaceId,
workflowId,
trigger: parameters.trigger,
steps: parameters.steps,
roleId,
});
if (parameters.stepPositions && parameters.stepPositions.length > 0) {
const positions = parameters.stepPositions.map((pos) => ({
id: pos.stepId === 'trigger' ? 'trigger' : pos.stepId,
position: pos.position,
}));
await this.workflowVersionService.updateWorkflowVersionPositions({
workflowVersionId,
positions,
workspaceId,
});
}
if (parameters.edges && parameters.edges.length > 0) {
for (const edge of parameters.edges) {
await this.workflowVersionEdgeService.createWorkflowVersionEdge({
source: edge.source === 'trigger' ? 'trigger' : edge.source,
target: edge.target,
workflowVersionId,
workspaceId,
});
}
}
if (parameters.activate) {
await this.workflowTriggerService.activateWorkflowVersion(
workflowVersionId,
);
await this.updateWorkflowStatus({
workspaceId,
workflowId,
workflowVersionId,
roleId,
});
}
return {
workflowId,
workflowVersionId,
name: parameters.name,
trigger: parameters.trigger,
steps: parameters.steps,
message: `Workflow "${parameters.name}" created successfully with ${parameters.steps.length} steps`,
};
} catch (error) {
return {
success: false,
error: error.message,
message: `Failed to create workflow "${parameters.name}": ${error.message}`,
};
}
},
};
tools.create_workflow_version_step = {
description:
'Create a new step in a workflow version. This adds a step to the specified workflow version with the given configuration.',
parameters: createWorkflowVersionStepSchema,
execute: async (parameters: CreateWorkflowVersionStepInput) => {
try {
return await this.workflowVersionStepService.createWorkflowVersionStep(
{
workspaceId,
input: parameters,
},
);
} catch (error) {
return {
success: false,
error: error.message,
message: `Failed to create workflow version step: ${error.message}`,
};
}
},
};
tools.update_workflow_version_step = {
description:
'Update an existing step in a workflow version. This modifies the step configuration.',
parameters: updateWorkflowVersionStepSchema,
execute: async (parameters: UpdateWorkflowVersionStepInput) => {
try {
return await this.workflowVersionStepService.updateWorkflowVersionStep(
{
workspaceId,
workflowVersionId: parameters.workflowVersionId,
step: parameters.step,
},
);
} catch (error) {
return {
success: false,
error: error.message,
message: `Failed to update workflow version step: ${error.message}`,
};
}
},
};
tools.delete_workflow_version_step = {
description:
'Delete a step from a workflow version. This removes the step and updates the workflow structure.',
parameters: deleteWorkflowVersionStepSchema,
execute: async (parameters: {
workflowVersionId: string;
stepId: string;
}) => {
try {
return await this.workflowVersionStepService.deleteWorkflowVersionStep(
{
workspaceId,
workflowVersionId: parameters.workflowVersionId,
stepIdToDelete: parameters.stepId,
},
);
} catch (error) {
return {
success: false,
error: error.message,
message: `Failed to delete workflow version step: ${error.message}`,
};
}
},
};
tools.create_workflow_version_edge = {
description:
'Create a connection (edge) between two workflow steps. This defines the flow between steps.',
parameters: createWorkflowVersionEdgeSchema,
execute: async (parameters: {
workflowVersionId: string;
source: string;
target: string;
}) => {
try {
return await this.workflowVersionEdgeService.createWorkflowVersionEdge(
{
source: parameters.source,
target: parameters.target,
workflowVersionId: parameters.workflowVersionId,
workspaceId,
},
);
} catch (error) {
return {
success: false,
error: error.message,
message: `Failed to create workflow version edge: ${error.message}`,
};
}
},
};
tools.delete_workflow_version_edge = {
description: 'Delete a connection (edge) between workflow steps.',
parameters: deleteWorkflowVersionEdgeSchema,
execute: async (parameters: {
workflowVersionId: string;
source: string;
target: string;
}) => {
try {
return await this.workflowVersionEdgeService.deleteWorkflowVersionEdge(
{
source: parameters.source,
target: parameters.target,
workflowVersionId: parameters.workflowVersionId,
workspaceId,
},
);
} catch (error) {
return {
success: false,
error: error.message,
message: `Failed to delete workflow version edge: ${error.message}`,
};
}
},
};
tools.create_draft_from_workflow_version = {
description:
'Create a new draft workflow version from an existing one. This allows for iterative workflow development.',
parameters: createDraftFromWorkflowVersionSchema,
execute: async (parameters: {
workflowId: string;
workflowVersionIdToCopy: string;
}) => {
try {
return await this.workflowVersionService.createDraftFromWorkflowVersion(
{
workspaceId,
workflowId: parameters.workflowId,
workflowVersionIdToCopy: parameters.workflowVersionIdToCopy,
},
);
} catch (error) {
return {
success: false,
error: error.message,
message: `Failed to create draft from workflow version: ${error.message}`,
};
}
},
};
tools.update_workflow_version_positions = {
description:
'Update the positions of multiple workflow steps. This is useful for reorganizing the workflow layout.',
parameters: updateWorkflowVersionPositionsSchema,
execute: async (parameters: UpdateWorkflowVersionPositionsInput) => {
try {
return await this.workflowVersionService.updateWorkflowVersionPositions(
{
workflowVersionId: parameters.workflowVersionId,
positions: parameters.positions,
workspaceId,
},
);
} catch (error) {
return {
success: false,
error: error.message,
message: `Failed to update workflow version step positions: ${error.message}`,
};
}
},
};
tools.activate_workflow_version = {
description:
'Activate a workflow version. This makes the workflow version active and available for execution.',
parameters: activateWorkflowVersionSchema,
execute: async (parameters: { workflowVersionId: string }) => {
try {
return await this.workflowTriggerService.activateWorkflowVersion(
parameters.workflowVersionId,
);
} catch (error) {
return {
success: false,
error: error.message,
message: `Failed to activate workflow version: ${error.message}`,
};
}
},
};
tools.deactivate_workflow_version = {
description:
'Deactivate a workflow version. This makes the workflow version inactive and unavailable for execution.',
parameters: deactivateWorkflowVersionSchema,
execute: async (parameters: { workflowVersionId: string }) => {
try {
return await this.workflowTriggerService.deactivateWorkflowVersion(
parameters.workflowVersionId,
);
} catch (error) {
return {
success: false,
error: error.message,
message: `Failed to deactivate workflow version: ${error.message}`,
};
}
},
};
tools.compute_step_output_schema = {
description:
'Compute the output schema for a workflow step. This determines what data the step produces. The step parameter must be a valid WorkflowTrigger or WorkflowAction with the correct settings structure for its type.',
parameters: computeStepOutputSchemaSchema,
execute: async (parameters: {
step: WorkflowTrigger | WorkflowAction;
}) => {
try {
return await this.workflowSchemaService.computeStepOutputSchema({
step: parameters.step,
workspaceId,
});
} catch (error) {
return {
success: false,
error: error.message,
message: `Failed to compute step output schema: ${error.message}`,
};
}
},
};
return tools;
}
private async createWorkflow({
workspaceId,
name,
roleId,
}: {
workspaceId: string;
name: string;
roleId: string;
}): Promise<string> {
const workflowRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
workspaceId,
'workflow',
{ roleId },
);
const workflowPosition =
await this.recordPositionService.buildRecordPosition({
value: 'first',
objectMetadata: {
isCustom: false,
nameSingular: 'workflow',
},
workspaceId,
});
const workflow = workflowRepository.create({
id: uuidv4(),
name,
statuses: [WorkflowStatus.DRAFT],
position: workflowPosition,
});
const savedWorkflow = await workflowRepository.save(workflow);
return savedWorkflow.id;
}
private async createWorkflowVersion({
workspaceId,
workflowId,
trigger,
steps,
roleId,
}: {
workspaceId: string;
workflowId: string;
trigger: WorkflowTrigger;
steps: WorkflowAction[];
roleId: string;
}): Promise<string> {
const workflowVersionRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
workspaceId,
'workflowVersion',
{ roleId },
);
const versionPosition =
await this.recordPositionService.buildRecordPosition({
value: 'first',
objectMetadata: {
isCustom: false,
nameSingular: 'workflowVersion',
},
workspaceId,
});
const workflowVersion = workflowVersionRepository.create({
id: uuidv4(),
workflowId,
name: 'v1',
status: WorkflowVersionStatus.DRAFT,
trigger,
steps,
position: versionPosition,
});
const savedWorkflowVersion =
await workflowVersionRepository.save(workflowVersion);
return savedWorkflowVersion.id;
}
private async updateWorkflowStatus({
workspaceId,
workflowId,
workflowVersionId,
roleId,
}: {
workspaceId: string;
workflowId: string;
workflowVersionId: string;
roleId: string;
}) {
const workflowRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
workspaceId,
'workflow',
{ roleId },
);
await workflowRepository.update(workflowId, {
statuses: [WorkflowStatus.ACTIVE],
lastPublishedVersionId: workflowVersionId,
});
}
}
@@ -0,0 +1,24 @@
import { Module } from '@nestjs/common';
import { RecordPositionModule } from 'src/engine/core-modules/record-position/record-position.module';
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';
import { WorkflowVersionModule } from 'src/modules/workflow/workflow-builder/workflow-version/workflow-version.module';
import { WorkflowTriggerModule } from 'src/modules/workflow/workflow-trigger/workflow-trigger.module';
import { WorkflowToolWorkspaceService } from './services/workflow-tool.workspace-service';
@Module({
imports: [
WorkflowVersionStepModule,
WorkflowVersionEdgeModule,
WorkflowVersionModule,
WorkflowTriggerModule,
WorkflowSchemaModule,
RecordPositionModule,
],
providers: [WorkflowToolWorkspaceService],
exports: [WorkflowToolWorkspaceService],
})
export class WorkflowToolsModule {}
@@ -10,7 +10,7 @@ import {
setupRepositoryMock,
} from './utils/agent-tool-test-utils';
describe('AgentToolService Integration', () => {
describe('AgentToolGeneratorService Integration', () => {
let context: AgentToolTestContext;
beforeEach(async () => {
@@ -132,21 +132,7 @@ describe('AgentToolService Integration', () => {
expect(Object.keys(tools)).toContain('http_request');
});
it('should return empty tools when role does not exist', async () => {
jest
.spyOn(context.agentService, 'findOneAgent')
.mockResolvedValue(context.testAgent as any);
jest.spyOn(context.roleRepository, 'findOne').mockResolvedValue(null);
const tools = await context.agentToolService.generateToolsForAgent(
context.testAgentId,
context.testWorkspaceId,
);
expect(tools).toEqual({});
});
it('should filter out workflow-related objects', async () => {
it('should filter out workflow-run objects', async () => {
const workflowObject = {
...context.testObjectMetadata,
nameSingular: 'workflow',
@@ -187,7 +173,7 @@ describe('AgentToolService Integration', () => {
context.testWorkspaceId,
);
expect(Object.keys(tools)).toHaveLength(1);
expect(Object.keys(tools)).toHaveLength(7);
});
});
@@ -5,11 +5,12 @@ import { type Repository } from 'typeorm';
import { ToolAdapterService } from 'src/engine/core-modules/ai/services/tool-adapter.service';
import { ToolService } from 'src/engine/core-modules/ai/services/tool.service';
import { RecordInputTransformerService } from 'src/engine/core-modules/record-transformer/services/record-input-transformer.service';
import { ToolRegistryService } from 'src/engine/core-modules/tool/services/tool-registry.service';
import { SendEmailTool } from 'src/engine/core-modules/tool/tools/send-email-tool/send-email-tool';
import { AgentHandoffExecutorService } from 'src/engine/metadata-modules/agent/agent-handoff-executor.service';
import { AgentHandoffService } from 'src/engine/metadata-modules/agent/agent-handoff.service';
import { AgentToolService } from 'src/engine/metadata-modules/agent/agent-tool.service';
import { AgentToolGeneratorService } from 'src/engine/metadata-modules/agent/agent-tool-generator.service';
import { type AgentEntity } from 'src/engine/metadata-modules/agent/agent.entity';
import { AgentService } from 'src/engine/metadata-modules/agent/agent.service';
import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
@@ -19,14 +20,14 @@ import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service';
import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { MessagingSendMessageService } from 'src/modules/messaging/message-import-manager/services/messaging-send-message.service';
import { getMockObjectMetadataEntity } from 'src/utils/__test__/get-object-metadata-entity.mock';
import { RecordInputTransformerService } from 'src/engine/core-modules/record-transformer/services/record-input-transformer.service';
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
import { MessagingSendMessageService } from 'src/modules/messaging/message-import-manager/services/messaging-send-message.service';
import { WorkflowToolWorkspaceService } from 'src/modules/workflow/workflow-tools/services/workflow-tool.workspace-service';
import { getMockObjectMetadataEntity } from 'src/utils/__test__/get-object-metadata-entity.mock';
export interface AgentToolTestContext {
module: TestingModule;
agentToolService: AgentToolService;
agentToolService: AgentToolGeneratorService;
agentService: AgentService;
objectMetadataService: ObjectMetadataService;
roleRepository: Repository<RoleEntity>;
@@ -48,7 +49,7 @@ export const createAgentToolTestModule =
const module = await Test.createTestingModule({
providers: [
AgentToolService,
AgentToolGeneratorService,
{
provide: AgentService,
useValue: {
@@ -126,6 +127,7 @@ export const createAgentToolTestModule =
provide: PermissionsService,
useValue: {
hasToolPermission: jest.fn(),
checkRolePermissions: jest.fn().mockReturnValue(true),
},
},
{
@@ -142,10 +144,18 @@ export const createAgentToolTestModule =
executeHandoff: jest.fn().mockResolvedValue({ success: true }),
},
},
{
provide: WorkflowToolWorkspaceService,
useValue: {
generateWorkflowTools: jest.fn().mockResolvedValue({}),
},
},
],
}).compile();
const agentToolService = module.get<AgentToolService>(AgentToolService);
const agentToolService = module.get<AgentToolGeneratorService>(
AgentToolGeneratorService,
);
const agentService = module.get<AgentService>(AgentService);
const objectMetadataService = module.get<ObjectMetadataService>(
ObjectMetadataService,
@@ -8,6 +8,44 @@
*/
export { TRIGGER_STEP_ID } from './constants/TriggerStepId';
export {
objectRecordSchema,
baseWorkflowActionSettingsSchema,
baseWorkflowActionSchema,
baseTriggerSchema,
workflowCodeActionSettingsSchema,
workflowSendEmailActionSettingsSchema,
workflowCreateRecordActionSettingsSchema,
workflowUpdateRecordActionSettingsSchema,
workflowDeleteRecordActionSettingsSchema,
workflowFindRecordsActionSettingsSchema,
workflowFormActionSettingsSchema,
workflowHttpRequestActionSettingsSchema,
workflowAiAgentActionSettingsSchema,
workflowFilterActionSettingsSchema,
workflowCodeActionSchema,
workflowSendEmailActionSchema,
workflowCreateRecordActionSchema,
workflowUpdateRecordActionSchema,
workflowDeleteRecordActionSchema,
workflowFindRecordsActionSchema,
workflowFormActionSchema,
workflowHttpRequestActionSchema,
workflowAiAgentActionSchema,
workflowFilterActionSchema,
workflowActionSchema,
workflowDatabaseEventTriggerSchema,
workflowManualTriggerSchema,
workflowCronTriggerSchema,
workflowWebhookTriggerSchema,
workflowTriggerSchema,
workflowRunStepStatusSchema,
workflowRunStateStepInfoSchema,
workflowRunStateStepInfosSchema,
workflowRunStateSchema,
workflowRunStatusSchema,
workflowRunSchema,
} from './schemas/workflow.schema';
export type {
WorkflowRunStepInfo,
WorkflowRunStepInfos,
@@ -1,36 +1,82 @@
import { FieldMetadataType } from 'twenty-shared/types';
import { StepStatus } from 'twenty-shared/workflow';
import { z } from 'zod';
import { FieldMetadataType } from '../../types/FieldMetadataType';
import { StepLogicalOperator } from '../../types/StepFilters';
import { ViewFilterOperand } from '../../types/ViewFilterOperand';
import { StepStatus } from '../types/WorkflowRunStateStepInfos';
// Base schemas
export const objectRecordSchema = z.record(z.any());
export const objectRecordSchema = z
.record(z.any())
.describe(
'Record data object. Use nested objects for relationships (e.g., "company": {"id": "{{reference}}"}). Common patterns:\n' +
'- Person: {"name": {"firstName": "John", "lastName": "Doe"}, "emails": {"primaryEmail": "john@example.com"}, "company": {"id": "{{trigger.object.id}}"}}\n' +
'- Company: {"name": "Acme Corp", "domainName": {"primaryLinkUrl": "https://acme.com"}}\n' +
'- Task: {"title": "Follow up", "status": "TODO", "assignee": {"id": "{{user.id}}"}}',
);
export const baseWorkflowActionSettingsSchema = z.object({
input: z.object({}).passthrough(),
outputSchema: z.object({}).passthrough(),
input: z
.object({})
.passthrough()
.describe('Input data for the workflow action. Structure depends on the action type.'),
outputSchema: z
.object({})
.passthrough()
.describe(
'Schema defining the output data structure. This data can be referenced in subsequent steps using {{stepId.fieldName}}.',
),
errorHandlingOptions: z.object({
retryOnFailure: z.object({
value: z.boolean(),
value: z.boolean().describe('Whether to retry the action if it fails.'),
}),
continueOnFailure: z.object({
value: z.boolean(),
value: z.boolean().describe('Whether to continue to the next step if this action fails.'),
}),
}),
});
export const baseWorkflowActionSchema = z.object({
id: z.string(),
name: z.string(),
valid: z.boolean(),
nextStepIds: z.array(z.string()).optional().nullable(),
position: z.object({ x: z.number(), y: z.number() }).optional().nullable(),
id: z
.string()
.describe('Unique identifier for the workflow step. Must be unique within the workflow.'),
name: z
.string()
.describe('Human-readable name for the workflow step. Should clearly describe what the step does.'),
valid: z
.boolean()
.describe('Whether the step configuration is valid. Set to true when all required fields are properly configured.'),
nextStepIds: z
.array(z.string())
.optional()
.nullable()
.describe('Array of step IDs that this step connects to. Leave empty or null for the final step.'),
position: z
.object({ x: z.number(), y: z.number() })
.optional()
.nullable()
.describe('Position coordinates for the step in the workflow diagram.'),
});
export const baseTriggerSchema = z.object({
name: z.string().optional(),
type: z.string(),
position: z.object({ x: z.number(), y: z.number() }).optional().nullable(),
nextStepIds: z.array(z.string()).optional().nullable(),
name: z
.string()
.optional()
.describe('Human-readable name for the trigger. Optional but recommended for clarity.'),
type: z
.enum(['DATABASE_EVENT', 'MANUAL', 'CRON', 'WEBHOOK'])
.describe(
'Type of trigger. DATABASE_EVENT for record changes, MANUAL for user-initiated, CRON for scheduled, WEBHOOK for external calls.',
),
position: z
.object({ x: z.number(), y: z.number() })
.optional()
.nullable()
.describe('Position coordinates for the trigger in the workflow diagram. Use (0, 0) for the trigger step.'),
nextStepIds: z
.array(z.string())
.optional()
.nullable()
.describe('Array of step IDs that the trigger connects to. These are the first steps in the workflow.'),
});
// Action settings schemas
@@ -56,8 +102,15 @@ export const workflowSendEmailActionSettingsSchema =
export const workflowCreateRecordActionSettingsSchema =
baseWorkflowActionSettingsSchema.extend({
input: z.object({
objectName: z.string(),
objectRecord: objectRecordSchema,
objectName: z
.string()
.describe(
'The name of the object to create a record in. Must be lowercase (e.g., "person", "company", "task").',
),
objectRecord: objectRecordSchema
.describe(
'The record data to create.',
)
}),
});
@@ -146,8 +199,23 @@ export const workflowAiAgentActionSettingsSchema =
export const workflowFilterActionSettingsSchema =
baseWorkflowActionSettingsSchema.extend({
input: z.object({
stepFilterGroups: z.array(z.any()),
stepFilters: z.array(z.any()),
stepFilterGroups: z.array(z.object({
id: z.string(),
logicalOperator: z.nativeEnum(StepLogicalOperator),
parentStepFilterGroupId: z.string().optional(),
positionInStepFilterGroup: z.number().optional(),
})),
stepFilters: z.array(z.object({
id: z.string(),
type: z.string(),
stepOutputKey: z.string(),
operand: z.nativeEnum(ViewFilterOperand),
value: z.string(),
stepFilterGroupId: z.string(),
positionInStepFilterGroup: z.number().optional(),
fieldMetadataId: z.string().optional(),
compositeFieldSubFieldName: z.string().optional(),
})),
}),
});
@@ -162,26 +230,20 @@ export const workflowSendEmailActionSchema = baseWorkflowActionSchema.extend({
settings: workflowSendEmailActionSettingsSchema,
});
export const workflowCreateRecordActionSchema = baseWorkflowActionSchema.extend(
{
type: z.literal('CREATE_RECORD'),
settings: workflowCreateRecordActionSettingsSchema,
},
);
export const workflowCreateRecordActionSchema = baseWorkflowActionSchema.extend({
type: z.literal('CREATE_RECORD'),
settings: workflowCreateRecordActionSettingsSchema,
});
export const workflowUpdateRecordActionSchema = baseWorkflowActionSchema.extend(
{
type: z.literal('UPDATE_RECORD'),
settings: workflowUpdateRecordActionSettingsSchema,
},
);
export const workflowUpdateRecordActionSchema = baseWorkflowActionSchema.extend({
type: z.literal('UPDATE_RECORD'),
settings: workflowUpdateRecordActionSettingsSchema,
});
export const workflowDeleteRecordActionSchema = baseWorkflowActionSchema.extend(
{
type: z.literal('DELETE_RECORD'),
settings: workflowDeleteRecordActionSettingsSchema,
},
);
export const workflowDeleteRecordActionSchema = baseWorkflowActionSchema.extend({
type: z.literal('DELETE_RECORD'),
settings: workflowDeleteRecordActionSettingsSchema,
});
export const workflowFindRecordsActionSchema = baseWorkflowActionSchema.extend({
type: z.literal('FIND_RECORDS'),
@@ -226,23 +288,45 @@ export const workflowActionSchema = z.discriminatedUnion('type', [
export const workflowDatabaseEventTriggerSchema = baseTriggerSchema.extend({
type: z.literal('DATABASE_EVENT'),
settings: z.object({
eventName: z.string(),
eventName: z
.string()
.regex(
/^[a-z][a-z0-9_]*\.(created|updated|deleted)$/,
'Event name must follow the pattern: objectName.action (e.g., "company.created", "person.updated")',
)
.describe(
'Event name in format: objectName.action (e.g., "company.created", "person.updated", "task.deleted"). Use lowercase object names.',
),
input: z.object({}).passthrough().optional(),
outputSchema: z.object({}).passthrough(),
outputSchema: z
.object({})
.passthrough()
.describe(
'Schema defining the output data structure. For database events, this includes the record that triggered the workflow accessible via {{trigger.object.fieldName}}.',
),
objectType: z.string().optional(),
fields: z.array(z.string()).optional().nullable(),
}),
});
}).describe(
'Database event trigger that fires when a record is created, updated, or deleted. The triggered record is accessible in workflow steps via {{trigger.object.fieldName}}.',
);
export const workflowManualTriggerSchema = baseTriggerSchema.extend({
type: z.literal('MANUAL'),
settings: z.object({
objectType: z.string().optional(),
outputSchema: z.object({}).passthrough(),
outputSchema: z
.object({})
.passthrough()
.describe(
'Schema defining the output data structure. When a record is selected, it is accessible via {{trigger.record.fieldName}}. When no record is selected, no data is available.',
),
icon: z.string().optional(),
isPinned: z.boolean().optional(),
}),
});
}).describe(
'Manual trigger that can be launched by the user. If a record is selected when launched, it is accessible via {{trigger.record.fieldName}}. If no record is selected, no data context is available.',
);
export const workflowCronTriggerSchema = baseTriggerSchema.extend({
type: z.literal('CRON'),