n8n
Version:
n8n Workflow Automation Tool
557 lines • 29.5 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.AgentsBuilderToolsService = void 0;
exports.getAgentConfigHash = getAgentConfigHash;
const tool_1 = require("@n8n/agents/tool");
const api_types_1 = require("@n8n/api-types");
const db_1 = require("@n8n/db");
const di_1 = require("@n8n/di");
const node_crypto_1 = require("node:crypto");
const zod_1 = require("zod");
const credential_types_1 = require("../../../credential-types");
const agent_task_service_1 = require("../agent-task.service");
const agents_tools_service_1 = require("../agents-tools.service");
const agents_service_1 = require("../agents.service");
const agent_config_composition_1 = require("../json-config/agent-config-composition");
const native_web_search_provider_tools_1 = require("../json-config/native-web-search-provider-tools");
const agent_repository_1 = require("../repositories/agent.repository");
const agent_secure_runtime_1 = require("../runtime/agent-secure-runtime");
const builder_model_lookup_service_1 = require("./builder-model-lookup.service");
const interactive_1 = require("./interactive");
const builder_tool_names_1 = require("./builder-tool-names");
const search_mcp_servers_tool_1 = require("./search-mcp-servers.tool");
const skill_body_template_1 = require("./skill-body-template");
const task_objective_template_1 = require("./task-objective-template");
const verify_mcp_server_tool_1 = require("./verify-mcp-server.tool");
const mcp_registry_service_1 = require("../../../modules/mcp-registry/registry/mcp-registry.service");
const oauth_service_1 = require("../../../oauth/oauth.service");
const EMPTY_INSTRUCTIONS_ERROR = {
path: '/instructions',
message: 'Refusing to write an agent with empty instructions. Ask the user what the agent should do before calling write_config or patch_config again.',
};
const STALE_CONFIG_ERROR = {
path: '(root)',
message: 'Agent config changed since you last read it. Call read_config and retry with the returned configHash.',
};
function rejectIfEmptyInstructions(config) {
if (!config.instructions.trim()) {
return { errors: [EMPTY_INSTRUCTIONS_ERROR] };
}
return null;
}
function rejectIfUnsupportedNativeWebSearch(config) {
const webSearch = config.config?.webSearch;
const requestsNativeWebSearch = webSearch?.enabled === true &&
(webSearch.provider === undefined ||
webSearch.provider === 'auto' ||
webSearch.provider === 'native');
if (!requestsNativeWebSearch || (0, native_web_search_provider_tools_1.hasNativeWebSearchProvider)(config.model))
return null;
return {
errors: [
{
path: '/config/webSearch/provider',
message: 'Native web search is only supported for Anthropic and OpenAI models. Use Brave or SearXNG fallback web search for this model.',
},
],
};
}
function isRecord(value) {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
function canonicalizeJson(value) {
if (Array.isArray(value)) {
return value.map((item) => canonicalizeJson(item));
}
if (!isRecord(value))
return value;
const sorted = {};
for (const key of Object.keys(value).sort()) {
sorted[key] = canonicalizeJson(value[key]);
}
return sorted;
}
function getAgentConfigHash(config) {
if (!config)
return null;
return (0, node_crypto_1.createHash)('sha256')
.update(JSON.stringify(canonicalizeJson(config)))
.digest('hex');
}
function snapshotFromConfig(config, updatedAt, versionId) {
return {
config,
configHash: getAgentConfigHash(config),
updatedAt,
versionId,
};
}
function applyNativeWebSearchBuilderDefaults(config) {
const providerTools = (0, native_web_search_provider_tools_1.getNativeWebSearchProviderTools)(config, {
includeDefaultArgs: true,
defaultEnabled: true,
});
const webSearch = config.config?.webSearch;
const fallbackWebSearch = webSearch?.enabled === true &&
(webSearch.provider === 'brave' || webSearch.provider === 'searxng');
const hasNativeWebSearch = !fallbackWebSearch && webSearch?.enabled !== false && (0, native_web_search_provider_tools_1.hasNativeWebSearchProvider)(config.model);
if (!hasNativeWebSearch) {
const { webSearch, ...restConfig } = config.config ?? {};
const { config: _config, providerTools: _providerTools, ...restAgentConfig } = config;
const normalizedConfig = {
...restConfig,
...(fallbackWebSearch ? { webSearch } : {}),
};
return {
...restAgentConfig,
...(Object.keys(normalizedConfig).length > 0 ? { config: normalizedConfig } : {}),
...(Object.keys(providerTools).length > 0 ? { providerTools } : {}),
};
}
return {
...config,
config: {
...(config.config ?? {}),
webSearch: { enabled: true },
},
providerTools,
};
}
let AgentsBuilderToolsService = class AgentsBuilderToolsService {
constructor(agentsService, secureRuntime, workflowRepository, agentsToolsService, builderModelLookupService, mcpRegistryService, oauthService, credentialTypes, agentTaskService, agentRepository) {
this.agentsService = agentsService;
this.secureRuntime = secureRuntime;
this.workflowRepository = workflowRepository;
this.agentsToolsService = agentsToolsService;
this.builderModelLookupService = builderModelLookupService;
this.mcpRegistryService = mcpRegistryService;
this.oauthService = oauthService;
this.credentialTypes = credentialTypes;
this.agentTaskService = agentTaskService;
this.agentRepository = agentRepository;
}
getTools(agentId, projectId, credentialProvider, user) {
return {
json: this.getJsonTools(agentId, projectId, credentialProvider, user),
shared: this.getSharedTools(agentId, projectId, credentialProvider),
};
}
getJsonTools(agentId, projectId, credentialProvider, user) {
const readConfigTool = new tool_1.Tool(builder_tool_names_1.BUILDER_TOOLS.READ_CONFIG)
.description('Read the latest persisted agent configuration and freshness metadata. ' +
'Returns { ok: true, config, configHash, updatedAt, versionId }. ' +
'Call this before every write_config or patch_config and use configHash as baseConfigHash.')
.input(zod_1.z.object({}))
.handler(async () => {
try {
return { ok: true, ...(await this.getConfigSnapshot(agentId, projectId)) };
}
catch (e) {
return {
ok: false,
errors: [{ path: '(root)', message: e instanceof Error ? e.message : String(e) }],
};
}
})
.build();
const writeConfigTool = new tool_1.Tool(builder_tool_names_1.BUILDER_TOOLS.WRITE_CONFIG)
.description('Create or replace the agent configuration by writing a complete JSON string. ' +
'Requires baseConfigHash from the immediately preceding read_config result, or from a stale retry response. ' +
'Do not use a configHash copied from the prompt snapshot. ' +
'Returns { ok: true, config, configHash, updatedAt, versionId } on success or ' +
'{ ok: false, stage, errors } with path, message, expected, received fields on failure.')
.input(zod_1.z.object({
json: zod_1.z.string().describe('Complete agent configuration as a JSON string'),
baseConfigHash: zod_1.z
.string()
.nullable()
.describe('configHash from the immediately preceding read_config result; null only if no config exists'),
}))
.handler(async ({ json, baseConfigHash }) => {
const parsed = (0, api_types_1.tryParseConfigJson)(json);
if (!parsed.ok) {
return { ok: false, errors: parsed.errors };
}
let snapshot;
try {
snapshot = await this.getConfigSnapshot(agentId, projectId);
}
catch (e) {
return {
ok: false,
stage: 'stale',
errors: [{ path: '(root)', message: e instanceof Error ? e.message : String(e) }],
};
}
if (baseConfigHash !== snapshot.configHash) {
return { ok: false, stage: 'stale', errors: [STALE_CONFIG_ERROR], ...snapshot };
}
const zodResult = api_types_1.RunnableAgentJsonConfigSchema.safeParse(parsed.data);
if (!zodResult.success) {
return { ok: false, errors: (0, api_types_1.formatZodErrors)(zodResult.error) };
}
const emptyInstructions = rejectIfEmptyInstructions(zodResult.data);
if (emptyInstructions) {
return { ok: false, errors: emptyInstructions.errors };
}
const unsupportedNativeWebSearch = rejectIfUnsupportedNativeWebSearch(zodResult.data);
if (unsupportedNativeWebSearch) {
return { ok: false, errors: unsupportedNativeWebSearch.errors };
}
const normalizedConfig = applyNativeWebSearchBuilderDefaults(zodResult.data);
try {
const result = await this.agentsService.updateConfig(agentId, projectId, normalizedConfig);
return {
ok: true,
...snapshotFromConfig(result.config, result.updatedAt, result.versionId),
};
}
catch (e) {
return {
ok: false,
stage: 'schema',
errors: [{ path: '(root)', message: e instanceof Error ? e.message : String(e) }],
};
}
})
.build();
const patchConfigTool = new tool_1.Tool(builder_tool_names_1.BUILDER_TOOLS.PATCH_CONFIG)
.description('Apply RFC 6902 JSON Patch operations to the current agent configuration. ' +
'Pass an array of patch operations as a JSON string. ' +
'Requires baseConfigHash from the immediately preceding read_config result, or from a stale retry response. ' +
'Do not use a configHash copied from the prompt snapshot. ' +
'Supported ops: add, remove, replace, move, copy, test. ' +
'Returns { ok: true, config, configHash, updatedAt, versionId } on success or ' +
'{ ok: false, stage, errors } on failure. ' +
'stage is "parse", "stale", "patch", or "schema".')
.input(zod_1.z.object({
operations: zod_1.z.string().describe('RFC 6902 JSON Patch operations array as a JSON string'),
baseConfigHash: zod_1.z
.string()
.nullable()
.describe('configHash from the immediately preceding read_config result; null only if no config exists'),
}))
.handler(async ({ operations, baseConfigHash, }) => {
const parsedOps = (0, api_types_1.tryParseConfigJson)(operations);
if (!parsedOps.ok) {
return { ok: false, stage: 'parse', errors: parsedOps.errors };
}
let snapshot;
try {
snapshot = await this.getConfigSnapshot(agentId, projectId);
}
catch (e) {
return {
ok: false,
stage: 'stale',
errors: [{ path: '(root)', message: e instanceof Error ? e.message : String(e) }],
};
}
if (baseConfigHash !== snapshot.configHash) {
return { ok: false, stage: 'stale', errors: [STALE_CONFIG_ERROR], ...snapshot };
}
if (!snapshot.config) {
return {
ok: false,
stage: 'patch',
errors: [{ path: '(root)', message: 'Agent has no JSON config yet.' }],
};
}
const jsonpatch = (await Promise.resolve().then(() => __importStar(require('fast-json-patch')))).default;
const ops = parsedOps.data;
const patchError = jsonpatch.validate(ops, snapshot.config);
if (patchError) {
const opPath = patchError.operation?.path ?? '(root)';
return {
ok: false,
stage: 'patch',
errors: [{ path: opPath, message: patchError.message ?? 'Invalid patch operation' }],
};
}
const patched = jsonpatch.applyPatch(jsonpatch.deepClone(snapshot.config), ops)
.newDocument;
const zodResult = api_types_1.RunnableAgentJsonConfigSchema.safeParse(patched);
if (!zodResult.success) {
return { ok: false, stage: 'schema', errors: (0, api_types_1.formatZodErrors)(zodResult.error) };
}
const emptyInstructions = rejectIfEmptyInstructions(zodResult.data);
if (emptyInstructions) {
return { ok: false, stage: 'schema', errors: emptyInstructions.errors };
}
const unsupportedNativeWebSearch = rejectIfUnsupportedNativeWebSearch(zodResult.data);
if (unsupportedNativeWebSearch) {
return { ok: false, stage: 'schema', errors: unsupportedNativeWebSearch.errors };
}
const normalizedConfig = applyNativeWebSearchBuilderDefaults(zodResult.data);
try {
const result = await this.agentsService.updateConfig(agentId, projectId, normalizedConfig);
return {
ok: true,
...snapshotFromConfig(result.config, result.updatedAt, result.versionId),
};
}
catch (e) {
return {
ok: false,
stage: 'schema',
errors: [{ path: '(root)', message: e instanceof Error ? e.message : String(e) }],
};
}
})
.build();
const listIntegrationTypesTool = new tool_1.Tool(builder_tool_names_1.BUILDER_TOOLS.LIST_INTEGRATION_TYPES)
.description("List integration types that can be added to the agent's `integrations` array. " +
'Returns every available chat platform with the list of ' +
'credential types it supports (`credentialTypes: string[]`) and builder guidance ' +
'(`capabilities`, `useIntegrationWhen`, `useNodeToolWhen`). ' +
'Use that guidance to decide whether the user needs a chat integration or a node tool. ' +
'Call this BEFORE asking the user for a credential. Then pick ONE entry from the ' +
'returned `credentialTypes` and pass it to `ask_credential` as the singular ' +
'`credentialType` arg.')
.input(zod_1.z.object({}))
.handler(async () => this.agentsService.listChatIntegrations())
.build();
const modelLookup = {
list: async (credentialId, credentialType, lookup) => await this.builderModelLookupService.list(user, credentialId, credentialType, lookup),
};
const tools = [
readConfigTool,
writeConfigTool,
patchConfigTool,
listIntegrationTypesTool,
(0, interactive_1.buildResolveLlmTool)({ credentialProvider, modelLookup }),
(0, interactive_1.buildAskCredentialTool)({
credentialProvider,
isCredentialTypeKnown: (credentialType) => this.credentialTypes.recognizes(credentialType),
}),
(0, interactive_1.buildAskLlmTool)(),
(0, interactive_1.buildAskQuestionTool)(),
(0, verify_mcp_server_tool_1.buildVerifyMcpServerTool)({
credentialProvider,
oauthService: this.oauthService,
projectId,
}),
(0, search_mcp_servers_tool_1.buildSearchMcpServersTool)({ mcpRegistryService: this.mcpRegistryService }),
];
return tools;
}
getSharedTools(agentId, projectId, credentialProvider) {
const buildCustomToolTool = new tool_1.Tool(builder_tool_names_1.BUILDER_TOOLS.BUILD_CUSTOM_TOOL)
.description('Compile and store a custom tool. Pass the complete TypeScript source ' +
'using `export default new Tool(...)` builder chain. The code is validated in a ' +
'sandbox and saved against the agent, but this does NOT register the tool in the ' +
'agent config — follow up with patch_config (or write_config) to add a ' +
'`{ type: "custom", id }` entry to `tools` so the agent actually uses it. ' +
'Returns { ok: true, id, descriptor } or { ok: false, errors }.')
.input(zod_1.z.object({
code: zod_1.z
.string()
.describe('Complete TypeScript source using export default new Tool(...)'),
}))
.handler(async ({ code }) => {
try {
const descriptor = await this.secureRuntime.describeToolSecurely(code);
const built = await this.agentsService.buildCustomTool(agentId, projectId, code, descriptor);
return { ok: true, id: built.id, descriptor };
}
catch (e) {
return {
ok: false,
errors: [{ message: e instanceof Error ? e.message : String(e) }],
};
}
})
.build();
const createSkillTool = new tool_1.Tool(builder_tool_names_1.BUILDER_TOOLS.CREATE_SKILL)
.description('Create and store an agent skill (a reusable, load-on-demand capability). Pass the skill name, a ' +
'routing description, and the full skill body. The description is what the runtime sees when ' +
'deciding when to load it, and the body MUST follow the required structured format (Overview, ' +
'Inputs, Steps, Rules, Example, Gotchas) filled with concrete content — see the body parameter ' +
'for the template. You MUST NOT call this with a vague description or a thin/placeholder body: ' +
'if you lack the domain detail to write a genuinely useful skill, ask the user clarifying ' +
'questions first. This does NOT attach the skill to the agent config; follow up with read_config ' +
'and patch_config (or write_config) to add a `{ type: "skill", id }` entry to `skills`. ' +
'Returns { ok: true, id, skill } or { ok: false, errors }.')
.systemInstruction('Never create a vague or placeholder skill. The description is the routing contract (what the ' +
'skill does + when to load it); the body must follow the required structured Markdown template ' +
'(Overview, Inputs, Steps, Rules, Example, Gotchas) with each applicable section filled in with ' +
'concrete, specific content. If you do not have enough domain detail to write a genuinely ' +
'useful skill, ask the user clarifying questions until you do before calling create_skill.')
.input(zod_1.z.object({
name: api_types_1.agentSkillSchema.shape.name.describe('Human-readable skill name'),
description: api_types_1.agentSkillSchema.shape.description.describe(skill_body_template_1.SKILL_DESCRIPTION_RULE),
body: api_types_1.agentSkillSchema.shape.instructions.describe(skill_body_template_1.SKILL_BODY_GUIDANCE),
}))
.handler(async ({ name, description, body, }) => {
const skill = { name, description, instructions: body };
try {
const created = await this.agentsService.createSkill(agentId, projectId, skill);
return { ok: true, id: created.id, skill: created.skill };
}
catch (e) {
return {
ok: false,
errors: [{ message: e instanceof Error ? e.message : String(e) }],
};
}
})
.build();
const createTaskTool = new tool_1.Tool(builder_tool_names_1.BUILDER_TOOLS.CREATE_TASK)
.description('Create a recurring scheduled task for the target agent (name + objective + cron schedule). ' +
'The objective is the exact message the agent receives on each run, so it must be precise and ' +
'self-contained, and it MUST follow the required structured format (Objective, Context, Steps, ' +
'Output, Constraints, Success criteria) with every section filled in — see the objective ' +
'parameter for the template. You MUST NOT call this tool with a vague, broad, or placeholder ' +
'objective, an objective missing any section, or an unclear schedule. First make sure you can ' +
'fill every section of the template and know how often/when it should run; if anything is ' +
'ambiguous, ask the user clarifying questions (ask_question with discrete options for choices, ' +
'or empty options for open-ended) and only call create_task once the objective is complete and the cadence ' +
'is known. This adds a `{ type: "task", id, enabled }` ref to the agent config (config.tasks) ' +
'and the task starts running once the agent is (re)published. Returns { ok: true, task } or ' +
'{ ok: false, errors }.')
.systemInstruction('Never create a task with a vague or placeholder objective. The objective must follow the ' +
'required structured Markdown template (Objective, Context, Steps, Output, Constraints, ' +
'Success criteria) with every section filled in with concrete content. If the user has not ' +
'given you enough detail to complete every section and set a clear schedule, ask clarifying ' +
'questions first. A task can only use tools the agent already has: if any step in the ' +
'objective requires a tool, integration, or web search the agent is missing, you MUST add ' +
'it to the agent config (patch_config/write_config) BEFORE calling create_task — otherwise ' +
'the task will fail at runtime.')
.input(zod_1.z.object({
name: api_types_1.agentTaskSchema.shape.name.describe('Short, human-readable task name.'),
objective: api_types_1.agentTaskSchema.shape.objective.describe(task_objective_template_1.TASK_OBJECTIVE_GUIDANCE),
cronExpression: api_types_1.agentTaskSchema.shape.cronExpression.describe('A 5-field cron expression for when the task runs, e.g. "0 9 * * 1-5" = weekdays at 09:00.'),
}))
.handler(async ({ name, objective, cronExpression, }) => {
const agent = await this.agentRepository.findByIdAndProjectId(agentId, projectId);
if (!agent) {
return { ok: false, errors: [{ message: 'Agent not found' }] };
}
try {
const task = await this.agentTaskService.create(agentId, {
name,
objective,
cronExpression,
enabled: true,
});
return { ok: true, task };
}
catch (e) {
return {
ok: false,
errors: [{ message: e instanceof Error ? e.message : String(e) }],
};
}
})
.build();
const listWorkflowsTool = new tool_1.Tool('list_workflows')
.description('List the n8n workflows that can be attached as tools via `type: "workflow"` in the agent config. ' +
'ALWAYS call this at the start — workflows are the preferred way to give agents real capabilities ' +
'(sending emails, creating calendar events, querying databases, calling APIs, etc.). ' +
'Only returns workflows with supported trigger types.')
.input(zod_1.z.object({}))
.handler(async () => {
const workflows = await this.workflowRepository.find({
select: ['id', 'name', 'nodes', 'active', 'updatedAt'],
where: { shared: { projectId } },
relations: ['shared'],
order: { updatedAt: 'DESC' },
take: 100,
});
const SUPPORTED_TRIGGERS = {
'n8n-nodes-base.manualTrigger': 'manual',
'n8n-nodes-base.executeWorkflowTrigger': 'executeWorkflow',
'n8n-nodes-base.chatTrigger': 'chat',
'n8n-nodes-base.scheduleTrigger': 'schedule',
'n8n-nodes-base.formTrigger': 'form',
};
const compatible = workflows
.map((w) => {
const triggerNode = (w.nodes ?? []).find((n) => SUPPORTED_TRIGGERS[n.type]);
if (!triggerNode)
return null;
return {
name: w.name,
active: w.active,
triggerType: SUPPORTED_TRIGGERS[triggerNode.type],
};
})
.filter(Boolean);
return { workflows: compatible };
})
.build();
return [
buildCustomToolTool,
createSkillTool,
createTaskTool,
listWorkflowsTool,
...this.agentsToolsService.getSharedTools(credentialProvider, 'Read-only inspection of available credentials. Use ask_credential to let the user ' +
'pick the credential to wire into a node tool — never copy ids from this list directly ' +
'into the config.'),
];
}
async getConfigSnapshot(agentId, projectId) {
const agent = await this.agentsService.findById(agentId, projectId);
if (!agent)
throw new Error('Agent not found');
const config = (0, agent_config_composition_1.composeJsonConfig)(agent);
return snapshotFromConfig(config, agent.updatedAt.toISOString(), agent.versionId);
}
};
exports.AgentsBuilderToolsService = AgentsBuilderToolsService;
exports.AgentsBuilderToolsService = AgentsBuilderToolsService = __decorate([
(0, di_1.Service)(),
__metadata("design:paramtypes", [agents_service_1.AgentsService,
agent_secure_runtime_1.AgentSecureRuntime,
db_1.WorkflowRepository,
agents_tools_service_1.AgentsToolsService,
builder_model_lookup_service_1.BuilderModelLookupService,
mcp_registry_service_1.McpRegistryService,
oauth_service_1.OauthService,
credential_types_1.CredentialTypes,
agent_task_service_1.AgentTaskService,
agent_repository_1.AgentRepository])
], AgentsBuilderToolsService);
//# sourceMappingURL=agents-builder-tools.service.js.map