n8n
Version:
n8n Workflow Automation Tool
2,531 lines • 124 kB
JavaScript
"use strict";
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 __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.InstanceAiAdapterService = void 0;
exports.resolveMetricProviders = resolveMetricProviders;
exports.buildEvaluationConfigDto = buildEvaluationConfigDto;
exports.evaluationConfigToSummary = evaluationConfigToSummary;
exports.evaluationConfigToDetail = evaluationConfigToDetail;
exports.resolveDataTableByIdOrName = resolveDataTableByIdOrName;
exports.truncateResultData = truncateResultData;
exports.extractExecutionResult = extractExecutionResult;
exports.formatExecutionError = formatExecutionError;
exports.truncateNodeOutput = truncateNodeOutput;
exports.extractNodeOutput = extractNodeOutput;
exports.extractExecutionDebugInfo = extractExecutionDebugInfo;
const node_crypto_1 = require("node:crypto");
const promises_1 = require("node:fs/promises");
const node_path_1 = __importDefault(require("node:path"));
const ai_utilities_1 = require("@n8n/ai-utilities");
const instance_ai_1 = require("@n8n/instance-ai");
const api_types_1 = require("@n8n/api-types");
const config_1 = require("@n8n/config");
const constants_1 = require("@n8n/constants");
const nanoid_1 = require("nanoid");
const extract_resolved_node_parameters_1 = require("./extract-resolved-node-parameters");
const instance_ai_settings_service_1 = require("./instance-ai-settings.service");
const workflow_templates_service_1 = require("./workflow-templates.service");
const instance_ai_run_pin_data_1 = require("./instance-ai-run-pin-data");
const node_definition_resolver_1 = require("./node-definition-resolver");
const web_research_1 = require("./web-research");
const db_1 = require("@n8n/db");
const backend_common_1 = require("@n8n/backend-common");
const backend_network_1 = require("@n8n/backend-network");
const di_1 = require("@n8n/di");
const permissions_1 = require("@n8n/permissions");
const typeorm_1 = require("@n8n/typeorm");
const n8n_workflow_1 = require("n8n-workflow");
const active_executions_1 = require("../../active-executions");
const conflict_error_1 = require("../../errors/response-errors/conflict.error");
const credentials_finder_service_1 = require("../../credentials/credentials-finder.service");
const credentials_service_1 = require("../../credentials/credentials.service");
const evaluation_config_service_1 = require("../../evaluation.ee/evaluation-config.service");
const llm_judge_provider_registry_1 = require("../../evaluation.ee/llm-judge-provider-registry");
const event_service_1 = require("../../events/event.service");
const execution_persistence_1 = require("../../executions/execution-persistence");
const license_1 = require("../../license");
const posthog_1 = require("../../posthog");
const load_nodes_and_credentials_1 = require("../../load-nodes-and-credentials");
const node_types_1 = require("../../node-types");
const agents_credential_provider_1 = require("../../modules/agents/adapters/agents-credential-provider");
const instance_ai_builder_delegate_adapter_1 = require("../../modules/agents/instance-ai-builder-delegate.adapter");
const node_catalog_1 = require("../../node-catalog");
const data_table_repository_1 = require("../../modules/data-table/data-table.repository");
const data_table_service_1 = require("../../modules/data-table/data-table.service");
const node_description_transform_1 = require("../../modules/mcp-registry/node-description-transform");
const source_control_preferences_service_ee_1 = require("../../modules/source-control.ee/source-control-preferences.service.ee");
const check_access_1 = require("../../permissions.ee/check-access");
const ai_gateway_service_1 = require("../../services/ai-gateway.service");
const folder_service_1 = require("../../services/folder.service");
const node_resource_explorer_service_1 = require("../../services/node-resource-explorer.service");
const project_service_ee_1 = require("../../services/project.service.ee");
const role_service_1 = require("../../services/role.service");
const n8n_core_1 = require("n8n-core");
const tag_service_1 = require("../../services/tag.service");
const workflow_finder_service_1 = require("../../workflows/workflow-finder.service");
const workflow_history_service_1 = require("../../workflows/workflow-history/workflow-history.service");
const workflow_service_1 = require("../../workflows/workflow.service");
const utils_1 = require("../../workflows/utils");
const workflow_service_ee_1 = require("../../workflows/workflow.service.ee");
const telemetry_1 = require("../../telemetry");
const workflow_runner_1 = require("../../workflow-runner");
function resolveDisplayedDefaults(nodeProperties, parameters, nodeType, typeVersion, desc) {
const stubNode = {
id: '',
name: '',
type: nodeType,
typeVersion,
parameters: parameters,
position: [0, 0],
};
const resolved = n8n_workflow_1.NodeHelpers.getNodeParameters(nodeProperties, parameters, true, false, stubNode, desc);
return resolved ?? parameters;
}
let httpCredentialHostsCache;
let InstanceAiAdapterService = class InstanceAiAdapterService {
async getNodesFromCache() {
if (this.nodesCache && Date.now() < this.nodesCache.expiresAt) {
return await this.nodesCache.promise;
}
const filePath = node_path_1.default.join(this.instanceSettings.staticCacheDir, 'types/nodes.json');
const promise = (0, promises_1.readFile)(filePath, 'utf-8').then((json) => (0, n8n_workflow_1.jsonParse)(json));
this.nodesCache = { promise, expiresAt: Date.now() + this.NODES_CACHE_TTL_MS };
promise.catch(() => {
this.nodesCache = null;
});
return await promise;
}
constructor(logger, globalConfig, workflowService, workflowFinderService, workflowRepository, sharedWorkflowRepository, projectRepository, executionRepository, credentialsService, credentialsFinderService, activeExecutions, workflowRunner, loadNodesAndCredentials, nodeTypes, instanceSettings, dataTableService, dataTableRepository, nodeResourceExplorerService, folderService, projectService, tagService, sourceControlPreferencesService, settingsService, workflowHistoryService, enterpriseWorkflowService, license, executionPersistence, eventService, roleService, telemetry, aiBuilderTemporaryWorkflowRepository, ssrfProtectionService, outboundHttp, aiGatewayService, workflowTemplatesService, nodeCatalogService, evaluationConfigService, llmJudgeProviderRegistry) {
this.globalConfig = globalConfig;
this.workflowService = workflowService;
this.workflowFinderService = workflowFinderService;
this.workflowRepository = workflowRepository;
this.sharedWorkflowRepository = sharedWorkflowRepository;
this.projectRepository = projectRepository;
this.executionRepository = executionRepository;
this.credentialsService = credentialsService;
this.credentialsFinderService = credentialsFinderService;
this.activeExecutions = activeExecutions;
this.workflowRunner = workflowRunner;
this.loadNodesAndCredentials = loadNodesAndCredentials;
this.nodeTypes = nodeTypes;
this.instanceSettings = instanceSettings;
this.dataTableService = dataTableService;
this.dataTableRepository = dataTableRepository;
this.nodeResourceExplorerService = nodeResourceExplorerService;
this.folderService = folderService;
this.projectService = projectService;
this.tagService = tagService;
this.sourceControlPreferencesService = sourceControlPreferencesService;
this.settingsService = settingsService;
this.workflowHistoryService = workflowHistoryService;
this.enterpriseWorkflowService = enterpriseWorkflowService;
this.license = license;
this.executionPersistence = executionPersistence;
this.eventService = eventService;
this.roleService = roleService;
this.telemetry = telemetry;
this.aiBuilderTemporaryWorkflowRepository = aiBuilderTemporaryWorkflowRepository;
this.ssrfProtectionService = ssrfProtectionService;
this.outboundHttp = outboundHttp;
this.aiGatewayService = aiGatewayService;
this.workflowTemplatesService = workflowTemplatesService;
this.nodeCatalogService = nodeCatalogService;
this.evaluationConfigService = evaluationConfigService;
this.llmJudgeProviderRegistry = llmJudgeProviderRegistry;
this.nodesCache = null;
this.NODES_CACHE_TTL_MS = 5 * 60 * 1000;
this.webResearchCache = new web_research_1.LRUCache({
maxEntries: 100,
ttlMs: 15 * 60 * 1000,
});
this.searchCache = new web_research_1.LRUCache({
maxEntries: 100,
ttlMs: 15 * 60 * 1000,
});
this.logger = logger.scoped('instance-ai');
this.allowSendingParameterValues = globalConfig.ai.allowSendingParameterValues;
this.loadNodesAndCredentials.addPostProcessor?.(async () => {
this.nodesCache = null;
});
}
createContext(user, options) {
const { searchProxyConfig, pushRef, threadId, projectId, credentialIdAllowlist, agentId, configEvalsEnabled, modelId, } = options ?? {};
void this.trackGatewayAvailability();
const builderDelegateAdapter = this.getBuilderDelegateAdapter();
return {
userId: user.id,
projectId,
modelId,
workflowService: this.createWorkflowAdapter(user, threadId, projectId),
executionService: this.createExecutionAdapter(user, pushRef, threadId),
credentialService: this.createCredentialAdapter(user, projectId, credentialIdAllowlist),
nodeService: this.createNodeAdapter(user),
dataTableService: this.createDataTableAdapter(user, projectId),
...(configEvalsEnabled && this.evaluationConfigService
? {
evaluationConfigService: this.createEvaluationConfigAdapter(this.evaluationConfigService, user),
}
: {}),
webResearchService: this.createWebResearchAdapter(user, searchProxyConfig),
workspaceService: this.createWorkspaceAdapter(user),
templatesService: this.getTemplatesService(),
workflowTemplateService: this.createWorkflowTemplateAdapter(),
licenseHints: this.buildLicenseHints(),
logger: this.logger,
nodeTypesProvider: this.nodeTypes,
outputSchemaLookup: this.loadNodesAndCredentials.createOutputSchemaLookup?.(),
allowSendingParameterValues: this.allowSendingParameterValues,
...(builderDelegateAdapter && agentId && projectId
? { agentBuilderTarget: { agentId, projectId } }
: {}),
...(builderDelegateAdapter && projectId
? {
builderDelegate: this.withBuilderCreateTelemetry(builderDelegateAdapter.createDelegate(user, projectId, new agents_credential_provider_1.AgentsCredentialProvider(this.credentialsService, projectId, user)), threadId),
}
: {}),
};
}
withBuilderCreateTelemetry(delegate, threadId) {
if (!threadId)
return delegate;
return {
...delegate,
createAgent: async (name) => {
const created = await delegate.createAgent(name);
this.telemetry.track('Builder created agent', {
thread_id: threadId,
agent_id: created.agentId,
project_id: created.projectId,
});
return created;
},
};
}
getBuilderDelegateAdapter() {
if (!di_1.Container.get(backend_common_1.ModuleRegistry).isActive('agents'))
return null;
try {
return di_1.Container.get(instance_ai_builder_delegate_adapter_1.InstanceAiBuilderDelegateAdapterService);
}
catch (error) {
this.logger.warn('Failed to resolve builder delegate adapter; agent building disabled', {
error: error instanceof Error ? error.message : String(error),
});
return null;
}
}
async getGatewayConfigOrNull() {
if (!this.license.isLicensed(constants_1.LICENSE_FEATURES.AI_GATEWAY))
return null;
try {
return await this.aiGatewayService.getGatewayConfig();
}
catch {
return null;
}
}
async isConfigEvalsEnabled(user) {
const flags = await di_1.Container.get(posthog_1.PostHogClient).getFeatureFlags(user);
return flags?.[api_types_1.CONFIG_EVALUATIONS_FLAG] === api_types_1.CONFIG_EVALUATIONS_ENABLED_VARIANT;
}
buildAiGatewayNodeMeta(config, nodeName) {
if (!config)
return undefined;
if (!config.nodes.includes(nodeName))
return undefined;
const meta = { supported: true };
const operations = config.supportedActions?.[nodeName];
if (operations && Object.keys(operations).length > 0)
meta.operations = operations;
const minVersion = config.minNodeTypeVersion?.[nodeName];
if (minVersion !== undefined)
meta.minVersion = minVersion;
const hiddenProperties = config.hiddenNodeProperties?.[nodeName];
if (hiddenProperties && hiddenProperties.length > 0)
meta.hiddenProperties = hiddenProperties;
return meta;
}
getTemplatesService() {
if (!this.templatesService) {
this.templatesService = new instance_ai_1.BuilderTemplatesService({
...(0, instance_ai_1.builderTemplatesOptionsFromEnv)({ logger: this.logger }),
cacheDir: node_path_1.default.join(this.instanceSettings.n8nFolder, 'n8n-sdk-templates'),
logger: this.logger,
});
}
return this.templatesService;
}
createWorkflowTemplateAdapter() {
const workflowTemplatesService = this.workflowTemplatesService;
return {
async getTemplate(templateId) {
return await workflowTemplatesService.getTemplate(templateId);
},
};
}
buildLicenseHints() {
const hints = [];
if (!this.license.isLicensed('feat:namedVersions')) {
hints.push('**Named workflow versions** — naming and describing workflow versions (update-workflow-version) is available on the Pro plan and above.');
}
if (!this.license.isLicensed('feat:folders')) {
hints.push('**Folders** — organizing workflows into folders (list-folders, create-folder, delete-folder, move-workflow-to-folder) is available on registered Community Edition or paid plans.');
}
return hints;
}
async trackGatewayAvailability() {
const config = await this.getGatewayConfigOrNull();
if (!config)
return;
this.telemetry.track('instance_ai_gateway_available', {
nodeCount: config.nodes.length,
credentialTypeCount: config.credentialTypes.length,
});
}
assertInstanceNotReadOnly(resourceType) {
if (this.sourceControlPreferencesService.getPreferences().branchReadOnly) {
throw new Error(`Cannot modify ${resourceType} on a protected instance. This instance is in read-only mode.`);
}
}
createProjectScopeHelpers(user, boundProjectId) {
const { projectRepository } = this;
let personalProjectIdPromise = null;
const getPersonalProjectId = async () => {
personalProjectIdPromise ??= projectRepository
.getPersonalProjectForUserOrFail(user.id)
.then((p) => p.id);
return await personalProjectIdPromise;
};
const assertProjectScope = async (scopes, projectId) => {
const allowed = await (0, check_access_1.userHasScopes)(user, scopes, false, { projectId });
if (!allowed) {
throw new Error('User does not have the required permissions in this project');
}
};
const resolveProjectId = async (scopes, providedProjectId) => {
const projectId = providedProjectId ?? boundProjectId ?? (await getPersonalProjectId());
await assertProjectScope(scopes, projectId);
return projectId;
};
const resolveBoundProjectId = async (scopes) => {
if (!boundProjectId) {
throw new n8n_workflow_1.UnexpectedError('Cannot create a resource: this Instance AI run has no bound project.');
}
await assertProjectScope(scopes, boundProjectId);
return boundProjectId;
};
return { getPersonalProjectId, assertProjectScope, resolveProjectId, resolveBoundProjectId };
}
createWorkflowAdapter(user, threadId, boundProjectId) {
const { workflowService, workflowFinderService, workflowRepository, sharedWorkflowRepository, aiBuilderTemporaryWorkflowRepository, workflowHistoryService, enterpriseWorkflowService, executionRepository, executionPersistence, license, allowSendingParameterValues, telemetry, } = this;
const logger = this.logger;
const assertNotReadOnly = () => this.assertInstanceNotReadOnly('workflows');
const { resolveBoundProjectId } = this.createProjectScopeHelpers(user, boundProjectId);
const redactParameters = !allowSendingParameterValues;
return {
async list(options) {
const filter = {
...(options?.status === 'all' ? {} : { isArchived: options?.status === 'archived' }),
...(options?.query ? { query: options.query } : {}),
...(options?.scope !== 'instance' && boundProjectId ? { projectId: boundProjectId } : {}),
};
const { workflows } = await workflowService.getMany(user, {
take: options?.limit ?? 50,
filter,
});
return workflows
.filter((wf) => 'versionId' in wf)
.map((wf) => ({
id: wf.id,
name: wf.name,
versionId: wf.versionId,
activeVersionId: wf.activeVersionId ?? null,
isArchived: wf.isArchived,
createdAt: wf.createdAt.toISOString(),
updatedAt: wf.updatedAt.toISOString(),
}));
},
async get(workflowId) {
const workflow = await workflowFinderService.findWorkflowForUser(workflowId, user, [
'workflow:read',
]);
if (!workflow) {
throw new Error(`Workflow ${workflowId} not found or not accessible`);
}
return await toWorkflowDetailWithChecksum(workflow, { redactParameters });
},
async archive(workflowId) {
assertNotReadOnly();
const result = await workflowService.archive(user, workflowId, { skipArchived: true });
if (!result) {
throw new Error(`Workflow ${workflowId} not found or not accessible`);
}
},
async unarchive(workflowId) {
assertNotReadOnly();
const result = await workflowService.unarchive(user, workflowId);
if (!result) {
throw new Error(`Workflow ${workflowId} not found or not accessible`);
}
},
async clearAiTemporary(workflowId) {
assertNotReadOnly();
const workflow = await workflowFinderService.findWorkflowForUser(workflowId, user, [
'workflow:update',
]);
if (!workflow)
return;
if (!(await aiBuilderTemporaryWorkflowRepository.existsForWorkflow(workflowId)))
return;
await aiBuilderTemporaryWorkflowRepository.unmark(workflowId);
},
async archiveIfAiTemporary(workflowId) {
assertNotReadOnly();
const workflow = await workflowFinderService.findWorkflowForUser(workflowId, user, [
'workflow:update',
]);
if (!workflow)
return false;
if (!(await aiBuilderTemporaryWorkflowRepository.existsForWorkflow(workflowId))) {
return false;
}
if (workflow.isArchived) {
await aiBuilderTemporaryWorkflowRepository.unmark(workflowId);
return false;
}
await workflowService.archive(user, workflowId, { skipArchived: true });
await aiBuilderTemporaryWorkflowRepository.unmark(workflowId);
return true;
},
async publish(workflowId, options) {
const wf = await workflowService.activateWorkflow(user, workflowId, {
versionId: options?.versionId,
name: options?.name,
description: options?.description,
source: 'n8n-ai',
});
if (!wf.activeVersionId) {
throw new Error(`Workflow ${workflowId} was not activated — no active version set`);
}
if (threadId) {
telemetry.track('Builder published workflow', {
thread_id: threadId,
workflow_id: workflowId,
executed_by: 'ai',
});
}
return { activeVersionId: wf.activeVersionId };
},
async unpublish(workflowId) {
await workflowService.deactivateWorkflow(user, workflowId, {
source: 'n8n-ai',
});
},
async getAsWorkflowJSON(workflowId, versionId) {
const wf = await workflowFinderService.findWorkflowForUser(workflowId, user, [
'workflow:read',
]);
if (!wf)
throw new Error(`Workflow ${workflowId} not found or not accessible`);
if (!versionId)
return toWorkflowJSON(wf, { redactParameters });
const version = await workflowHistoryService.getVersion(user, workflowId, versionId);
return toWorkflowJSON(wf, { redactParameters, graph: version });
},
async getWorkflowHead(workflowId) {
const head = await workflowFinderService.findWorkflowHeadForUser(workflowId, user, [
'workflow:read',
]);
if (!head)
throw new Error(`Workflow ${workflowId} not found or not accessible`);
return { versionId: head.versionId, updatedAt: head.updatedAt.getTime() };
},
async getWorkflowSnapshot(workflowId) {
const wf = await workflowFinderService.findWorkflowForUser(workflowId, user, [
'workflow:read',
]);
if (!wf)
throw new Error(`Workflow ${workflowId} not found or not accessible`);
return {
json: toWorkflowJSON(wf, { redactParameters }),
versionId: wf.versionId,
updatedAt: wf.updatedAt.getTime(),
};
},
async getLatestRunData(workflowId) {
const accessible = await workflowFinderService.findWorkflowForUser(workflowId, user, [
'workflow:read',
]);
if (!accessible)
return null;
const [latest] = await executionRepository.find({
select: ['id'],
where: { workflowId },
order: { startedAt: 'DESC' },
take: 1,
});
if (!latest)
return null;
const execution = await executionPersistence.findSingleExecution(latest.id, {
includeData: true,
unflattenData: true,
});
return execution?.data?.resultData?.runData ?? null;
},
async createFromWorkflowJSON(json, options) {
assertNotReadOnly();
const projectId = await resolveBoundProjectId(['workflow:create']);
const settings = (json.settings ?? {});
if (settings.redactionPolicy !== undefined && settings.redactionPolicy !== 'none') {
const canUpdateRedaction = await (0, check_access_1.userHasScopes)(user, ['workflow:enableRedaction'], false, { projectId });
if (!canUpdateRedaction) {
delete settings.redactionPolicy;
}
}
const newWorkflow = workflowRepository.create({
name: json.name,
nodes: [],
connections: {},
settings,
active: false,
versionId: (0, node_crypto_1.randomUUID)(),
});
const saved = await workflowRepository.manager.transaction(async (transactionManager) => {
const workflow = await transactionManager.save(db_1.WorkflowEntity, newWorkflow);
await sharedWorkflowRepository.makeOwner([workflow.id], projectId, transactionManager);
if (options?.markAsAiTemporary) {
if (!threadId) {
throw new n8n_workflow_1.UnexpectedError('Cannot mark AI-builder temporary workflow without a thread ID');
}
await aiBuilderTemporaryWorkflowRepository.mark(workflow.id, threadId, transactionManager);
}
return workflow;
});
const nodes = sanitizeCredentialReferencesForSave(json.nodes);
let updateData = workflowRepository.create({
name: json.name,
nodes: nodes,
connections: json.connections,
settings,
pinData: (0, instance_ai_run_pin_data_1.sdkPinDataToRuntime)(json.pinData),
nodeGroups: sdkNodeGroupsToRuntime(json.nodeGroups),
});
let updated;
try {
if (license.isSharingEnabled()) {
updateData = await enterpriseWorkflowService.preventTampering(updateData, saved.id, user);
}
updated = await workflowService.update(user, updateData, saved.id, {
source: 'n8n-ai',
});
}
catch (error) {
logger.warn('AI-builder workflow save failed', {
threadId,
workflowId: saved.id,
error: error instanceof Error ? error.message : String(error),
});
try {
const archived = await workflowService.archive(user, saved.id, { skipArchived: true });
if (archived && options?.markAsAiTemporary) {
await aiBuilderTemporaryWorkflowRepository.unmark(saved.id);
}
}
catch (cleanupError) {
logger.warn('Failed to clean up AI-builder workflow shell after create failure', {
threadId,
workflowId: saved.id,
error: cleanupError instanceof Error ? cleanupError.message : String(cleanupError),
});
}
throw error;
}
if (threadId) {
telemetry.track('Builder created workflow', {
thread_id: threadId,
workflow_id: updated.id,
});
}
return await toWorkflowDetailWithChecksum(updated, { redactParameters });
},
async updateFromWorkflowJSON(workflowId, json, options) {
assertNotReadOnly();
const settings = (json.settings ?? {});
if (settings.redactionPolicy !== undefined) {
const [existingWorkflow, ownerProject] = await Promise.all([
workflowRepository.findOne({ where: { id: workflowId } }),
sharedWorkflowRepository.getWorkflowOwningProject(workflowId),
]);
const currentPolicy = existingWorkflow?.settings?.redactionPolicy;
if (settings.redactionPolicy !== currentPolicy) {
const requiredScopes = (0, utils_1.getRequiredRedactionScopes)(currentPolicy, settings.redactionPolicy);
const canUpdateRedaction = ownerProject &&
(await (0, check_access_1.userHasScopes)(user, requiredScopes, false, { projectId: ownerProject.id }));
if (!canUpdateRedaction) {
delete settings.redactionPolicy;
}
}
}
const nodes = sanitizeCredentialReferencesForSave(json.nodes);
let updateData = workflowRepository.create({
name: json.name,
nodes: nodes,
connections: json.connections,
settings,
pinData: (0, instance_ai_run_pin_data_1.sdkPinDataToRuntime)(json.pinData),
nodeGroups: sdkNodeGroupsToRuntime(json.nodeGroups),
});
let updated;
try {
if (license.isSharingEnabled()) {
updateData = await enterpriseWorkflowService.preventTampering(updateData, workflowId, user);
}
updated = await workflowService.update(user, updateData, workflowId, {
source: 'n8n-ai',
...(options?.expectedChecksum ? { expectedChecksum: options.expectedChecksum } : {}),
});
}
catch (error) {
if (error instanceof conflict_error_1.ConflictError) {
throw new instance_ai_1.WorkflowSaveConflictError(workflowId);
}
logger.warn('AI-builder workflow save failed', {
threadId,
workflowId,
error: error instanceof Error ? error.message : String(error),
});
throw error;
}
if (threadId) {
telemetry.track('Builder modified workflow', {
thread_id: threadId,
workflow_id: workflowId,
});
}
return await toWorkflowDetailWithChecksum(updated, { redactParameters });
},
async listVersions(workflowId, options) {
const take = options?.limit ?? 20;
const skip = options?.skip ?? 0;
const versions = await workflowHistoryService.getList(user, workflowId, take, skip);
const workflow = await workflowFinderService.findWorkflowForUser(workflowId, user, [
'workflow:read',
]);
const activeVersionId = workflow?.activeVersionId ?? null;
const currentDraftVersionId = workflow?.versionId ?? null;
return versions.map((v) => ({
versionId: v.versionId,
name: v.name ?? null,
description: v.description ?? null,
authors: v.authors,
createdAt: v.createdAt.toISOString(),
autosaved: v.autosaved ?? false,
isActive: v.versionId === activeVersionId,
isCurrentDraft: v.versionId === currentDraftVersionId,
}));
},
async getVersion(workflowId, versionId) {
const version = await workflowHistoryService.getVersion(user, workflowId, versionId);
const workflow = await workflowFinderService.findWorkflowForUser(workflowId, user, [
'workflow:read',
]);
const activeVersionId = workflow?.activeVersionId ?? null;
const currentDraftVersionId = workflow?.versionId ?? null;
return {
versionId: version.versionId,
name: version.name ?? null,
description: version.description ?? null,
authors: version.authors,
createdAt: version.createdAt.toISOString(),
autosaved: version.autosaved ?? false,
isActive: version.versionId === activeVersionId,
isCurrentDraft: version.versionId === currentDraftVersionId,
nodes: (version.nodes ?? []).map((n) => ({
name: n.name,
type: n.type,
typeVersion: n.typeVersion,
parameters: redactParameters ? undefined : n.parameters,
position: n.position,
})),
connections: version.connections,
};
},
async restoreVersion(workflowId, versionId) {
const version = await workflowHistoryService.getVersion(user, workflowId, versionId);
const updateData = workflowRepository.create({
nodes: version.nodes,
connections: version.connections,
nodeGroups: version.nodeGroups,
});
await workflowService.update(user, updateData, workflowId, {
source: 'n8n-ai',
});
},
...(this.license.isLicensed('feat:namedVersions')
? {
async updateVersion(workflowId, versionId, data) {
await workflowHistoryService.updateVersionForUser(user, workflowId, versionId, data);
},
}
: {}),
};
}
createExecutionAdapter(user, pushRef, threadId) {
const { workflowFinderService, workflowRunner, activeExecutions, executionRepository, nodeTypes, allowSendingParameterValues, roleService, telemetry, logger, globalConfig, } = this;
const assertNotReadOnly = () => this.assertInstanceNotReadOnly('executions');
const DEFAULT_TIMEOUT_MS = 5 * constants_1.Time.minutes.toMilliseconds;
const MAX_TIMEOUT_MS = 10 * constants_1.Time.minutes.toMilliseconds;
const assertExecutionAccess = async (executionId, scopes = ['workflow:read']) => {
const execution = await executionRepository.findSingleExecution(executionId, {
includeData: false,
});
if (!execution) {
throw new Error(`Execution ${executionId} not found`);
}
const workflow = await workflowFinderService.findWorkflowForUser(execution.workflowId, user, scopes);
if (!workflow) {
throw new Error(`Execution ${executionId} not found`);
}
return execution;
};
return {
async list(options) {
const scope = 'workflow:read';
const projectRoles = await roleService.rolesWithScope('project', [scope]);
const workflowRoles = await roleService.rolesWithScope('workflow', [scope]);
const sharingOptions = {
scopes: [scope],
projectRoles,
workflowRoles,
};
const query = {
kind: 'range',
range: { limit: options?.limit ?? 20, lastId: undefined, firstId: undefined },
order: { startedAt: 'DESC' },
user,
sharingOptions,
...(options?.workflowId ? { workflowId: options.workflowId } : {}),
...(options?.status
? {
status: [options.status],
}
: {}),
};
const executions = await executionRepository.findManyByRangeQuery(query);
return executions.map((e) => ({
id: e.id,
workflowId: e.workflowId,
workflowName: e.workflowName ?? '',
status: e.status,
startedAt: String(e.startedAt ?? ''),
finishedAt: e.stoppedAt ? String(e.stoppedAt) : undefined,
mode: e.mode,
}));
},
async run(workflowId, inputData, options) {
assertNotReadOnly();
const workflow = await workflowFinderService.findWorkflowForUser(workflowId, user, [
'workflow:execute',
]);
if (!workflow) {
throw new Error(`Workflow ${workflowId} not found or not accessible`);
}
const nodes = workflow.nodes ?? [];
const triggerNode = options?.triggerNodeName
? (nodes.find((n) => n.name === options.triggerNodeName) ?? findTriggerNode(nodes))
: findTriggerNode(nodes);
const timeoutMs = Math.min(options?.timeout ?? DEFAULT_TIMEOUT_MS, MAX_TIMEOUT_MS);
const runData = {
executionMode: triggerNode
? getExecutionModeForTrigger(triggerNode)
: 'manual',
workflowData: {
...workflow,
settings: {
...workflow.settings,
saveManualExecutions: true,
saveDataSuccessExecution: 'all',
saveDataErrorExecution: 'all',
executionTimeout: Math.ceil(timeoutMs / 1000),
},
},
userId: user.id,
pushRef,
};
const pinDataPlan = (0, instance_ai_run_pin_data_1.buildInstanceAiRunPinDataPlan)({
workflowPinData: workflow.pinData ?? {},
verificationPinData: options?.verificationPinData,
inputData,
triggerNode,
});
if (pinDataPlan.startNodeName) {
runData.startNodes = [{ name: pinDataPlan.startNodeName, sourceData: null }];
}
else if (triggerNode) {
runData.triggerToStartFrom = { name: triggerNode.name };
}
if (pinDataPlan.runPinData) {
runData.pinData = pinDataPlan.runPinData;
}
if (pinDataPlan.triggerExecutionData) {
runData.executionData = pinDataPlan.triggerExecutionData;
}
runData.source = 'instance_ai';
runData.telemetryMetadata = {
mockDataSources: pinDataPlan.mockDataSources,
};
if (runData.executionData) {
runData.executionData.manualData = {
...runData.executionData.manualData,
userId: user.id,
source: runData.source,
};
}
const offloadingManualExecutionsInQueueMode = globalConfig.executions?.mode === 'queue' &&
process.env.OFFLOAD_MANUAL_EXECUTIONS_TO_WORKERS === 'true';
if (runData.executionMode === 'manual' &&
offloadingManualExecutionsInQueueMode &&
!runData.executionData) {
runData.executionData = (0, n8n_workflow_1.createRunExecutionData)({
startData: { startNodes: runData.startNodes },
resultData: { pinData: runData.pinData, runData: null },
manualData: {
userId: runData.userId,
triggerToStartFrom: runData.triggerToStartFrom,
source: runData.source,
},
executionData: null,
});
}
const trackBuilderExecutedWorkflow = (status, error) => {
if (!threadId)
return;
telemetry.track('Builder executed workflow', {
thread_id: threadId,
workflow_id: workflowId,
executed_by: 'ai',
pinned_node_count: Object.keys(runData.pinData ?? {}).length,
exec_type: runData.executionMode,
status,
...(error ? { error } : {}),
});
};
try {
const executionId = await workflowRunner.run(runData);
const pruneVerificationPins = async (executedNodeNames) => {
try {
await (0, instance_ai_run_pin_data_1.pruneUnreachedVerificationPinData)({
executionId,
verificationPinData: pinDataPlan.verificationPinData,
nonVerificationPinData: pinDataPlan.nonVerificationPinData,
executedNodeNames,
});
}
catch (error) {
logger.warn('Failed to prune verification pin data from execution', {
executionId,
error: error instanceof Error ? error.message : String(error),
});
}
};
const abortSignal = options?.abortSignal;
if (activeExecutions.has(executionId)) {
let timeoutId;
const timeoutPromise = new Promise((_, reject) => {
timeoutId = setTimeout(() => {
reject(new Error(`Execution timed out after ${timeoutMs}ms`));
}, timeoutMs);
});
let onAbort;
const abortPromise = abortSignal === undefined
? undefined
: new Promise((_, reject) => {
onAbort = () => {
const error = new Error(typeof abortSignal.reason === 'string'
? abortSignal.reason
: 'This operation was aborted');
error.name = 'AbortError';
reject(error);
};
if (abortSignal.aborted) {
onAbort();
return;
}
abortSignal.addEventListener('abort', onAbort, { once: true });
});
try {
await Promise.race([
activeExecutions.getPostExecutePromise(executionId),
timeoutPromise,
...(abortPromise ? [abortPromise] : []),
]);
clearTimeout(timeoutId);
if (onAbort)
abortSignal?.removeEventListener('abort', onAbort);
}
catch (error) {
clearTimeout(timeoutId);
if (onAbort)
abortSignal?.removeEventListener('abort', onAbort);
const isTimeout = error instanceof Error && error.message.includes('timed out');
const isAbort = error instanceof Error &&
(error.name === 'AbortError' || abortSignal?.aborted === true);
if (isTimeout || isAbort) {
try {
activeExecutions.stopExecution(executionId, isAbort
? new n8n_workflow_1.ManualExecutionCancelledError(executionId)
: new n8n_workflow_1.TimeoutExecutionCancelledError(executionId));
}
catch {
}
const result = {
executionId,
status: 'error',
error: isAbort
? 'Execution was cancelled'
: `Execution timed out after ${timeoutMs}ms and was cancelled`,
};
await pruneVerificationPins();
trackBuilderExecutedWorkflow(result.status, result.error);
return result;
}
throw error;
}
}
const result = await extractExecutionResult(executionId, allowSendingParameterValues);
await pruneVerificationPins(result.executedNodeNames);
trackBuilderExecutedWorkflow(result.status, result.error);
return result;
}
catch (error) {
trackBuilderExecutedWorkflow('error', error instanceof Error ? error.message : String(error));
throw error;
}
},
async getStatus(executionId) {
await assertExecutionAccess(executionId);
const isRunning = activeExecutions.has(executionId);
if (isRunning) {
return { executionId, status: 'running' };
}
return await extractExecutionResult(executionId, allowSendingParameterValues);
},
async getResult(executionId) {
await assertExecutionAccess(executionId);
if (activeExecutions.has(executionId)) {
await activeExecutions.getPostExecutePromise(executionId);
}
return await extractExecutionResult(executionId, allowSendingParameterValues);
},
async stop(executionId) {
assertNotReadOnly();
await assertExecutionAccess(executionId, ['workflow:execute']);
if (!activeExecutions.has(executionId)) {
return {
success: false,
message: `Execution ${executionId} is not currently running`,
};
}
try {
activeExecutions.stopExecution(executionId, new n8n_workflow_1.TimeoutExecutionCancelledError(executionId));
return { success: true, message: `Execution ${executionId} cancelled` };
}
catch {
return {
success: false,
message: `Failed to cancel execution ${executionId}`,
};
}
},
async getDebugInfo(executionId) {
await assertExecutionAccess(executionId);
return await extractExecutionDebugInfo(executionId, allowSendingParameterValues, nodeTypes);
},
async getNodeOutput(executionId, nodeName, options) {
await assertExecutionAccess(executionId);
if (!allowSendingParameterValues) {
return {
nodeName,
items: [],
totalItems: 0,
returned: { from: 0, to: 0 },
};
}
return await extractNodeOutput(executionId, nodeName, options);
},
getResolvedNodeParameters: async (executionId, nodeName, options) => {
await assertExecutionAccess(executionId);
if (!allowSendingParameterValues) {
return {
nodeName,
runIndex: options?.runIndex ?? 0,
itemIndex: options?.itemIndex ?? 0,
parameters: null,
resolved: null,
failedExpressions: [],
emptyResolutions: [],
suppressed: 'parameter-values-disabled',
};
}
return await (0, extract_resolved_node_parameters_1.extractResolvedNodeParameters)(nodeTypes, executionId, nodeName, options);
},
};
}
createCredentialAdapter(user, boundProjectId, credentialIdAllowlist) {
const { credentialsService, credentialsFinderService, loadNodesAndCredentials } = this;
const getGatewayConfig = async () => await this.getGatewayConfigOrNull();
const adapter = {
async list(options) {
if (boundProjectId) {
const scoped = await credentialsService.getCredentialsAUserCanUseInAWorkflow(user, {
projectId: boundProjectId,
});
const filtered = options?.type ? scoped.filter((c) => c.type === options.type) : scoped;
return filtered.map((c) => ({ id: c.id, name: c.name, type: c.type }));
}
if (options?.workflowId || options?.projectId) {
const scoped = options.workflowId
? await credentialsService.getCredentialsAUserCanUseInAWorkflow(user, {
workflowId: options.workflowId,
})
: await credentialsService.getCredentialsAUserCanUseInAWorkflow(user, {
projectId: options.projectId,
});
const filtered = options.type ? scoped.filter((c) => c.type === options.type) : scoped;
return filtered.map((c) => ({
id: c.id,
name: c.name,
type: c.type,
}));
}
const credentials = await credentialsService.getMany(user, {
listQueryOptions: {
filter: options?.type ? { type: options.type } : undefined,
},
includeGlobal: true,
});
return credentials.map((c) => ({
id: c.id,
name: c.name,
type: c.type,
}));
},
async get(credentialId) {
const credential = await credentialsService.getOne(user, credentialId, false);
return {
id: credential.id,
name: credential.name,
type: credential.type,
};
},
async delete(credentialId) {
await credentialsService.delete(user, credentialId);
},
async test(credentialId) {
const credential = await credentialsFinderService.findCredentialForUser(credentialId, user, ['credential:read']);
if (!credential) {
throw new Error(`Credential ${credentialId} not found or not accessible`);
}
const credentialsToTest = {
id: credential.id,
name: credential.name,
type: credential.type,
data: await credentialsService.decrypt(credential, true),
};
const result = await credentialsService.test(user.id, credentialsToTest);
return {
success: result.status === 'OK',
message: result.message,
};
},
async isTestable(credentialType) {
try {
const credClass = loadNodesAndCredentials.getCredential(credentialType);
if (credClass.type.test)
return true;
const known = loadNodesAndCredentials.knownCredentials;
const supportedNodes = known[credentialType]?.supportedNodes ?? [];
for (const nodeName of supportedNodes) {
try {
const loaded = loadNodesAndCredentials.getNode(nodeName);
const nodeInstance = loaded.type;
const nodeDesc = 'nodeVersions' in nodeInstance
? Object.values(nodeInstance.nodeVersions).pop()?.description
: nodeInstance.description;
const hasTestedBy = nodeDesc?.credentials?.some((cred) => cred.name === credentialType && cred.testedBy);
if (hasTestedBy)
return true;
}
catch {
continue;
}
}
return false;
}
catch {
return false;
}
},
async getDocumentationUrl(credentialType) {
try {
const credClass = loadNodesAndCredentials.getCredential(credentialType);
const slug = credClass.type.documentationUrl;
if (!slug)
return null;
if (slug.startsWith('http'))
return slug;
return `https://docs.n8n.io/integrations/builtin/credentials/${slug}/`;
}
catch {
return null;
}
},
getCredentialFields(credentialType) {
try {
const allTypes = [credentialType];
const known = loadNodesAndCredentials.knownCredentials;
for (const typeName of allTypes) {
const extendsArr = known[typeName]?.extends ?? [];
allTypes.push(...extendsArr);
}
const fields = [];
const seen = new Set();
for (const typeName of allTypes) {
try {
const credClass = loadNodesAndCredentials.getCredential(typeName);
for (const prop of credClass.type.properties) {
if (prop.type === 'hidden' || seen.has(prop.name))
continue;
seen.add(prop.name);
fields.push({
name: prop.name,
displayName: prop.displayName,
type: prop.type,
required: prop.required ?? false,
description: prop.description,
});
}
}
catch {
}
}
return fields;
}
catch {
return [];
}
},
async searchCredentialTypes(query) {
const q = query.toLowerCase().trim();
if (!q)
return [];
const known = loadNodesAndCredentials.knownCredentials;
const results = [];
for (const typeName of Object.keys(known)) {
if (typeName.toLowerCase().includes(q)) {
try {
const credClass = loadNodesAndCredentials.getCredential(typeName);
results.push({
type: typeName,
displayName: credClass.type.displayName,
});
}
catch {
results.push({ type: typeName, displayName: typeName });
}
continue;
}
try {
const credClass = loadNodesAndCredentials.getCredential(typeName);
if (credClass.type.displayName.toLowerCase().includes(q)) {
results.push({
type: typeName,
displayName: credClass.type.displayName,
});
}
}
catch {
}
}
return results;
},
async listHttpCredentialHosts() {
if (httpCredentialHostsCache)
return httpCredentialHostsCache;
const { knownCredentials } = loadNodesAndCredentials;
const result = [];
for (const typeName of Object.keys(knownCredentials)) {
let credType;
try {
credType = loadNodesAndCredentials.getCredential(typeName).type;
}
catch {
continue;
}
const usableInHttpNode = Boolean(credType.authenticate) ||
(knownCredentials[typeName]?.extends ?? []).some((parent) => parent === 'oAuth2Api' || parent === 'oAuth1Api');
if (!usableInHttpNode)
continue;
const hosts = (0, instance_ai_1.deriveCredentialHosts)(credType);
if (hosts.length === 0)
continue;
result.push({ type: typeName, displayName: credType.displayName, hosts });
}
httpCredentialHostsCache = result;
return result;
},
async getAccountContext(credentialId) {
const credential = await credentialsFinderService.findCredentialForUser(credentialId, user, ['credential:read']);
if (!credential) {
return { accountIdentifier: undefined };
}
const mask = (id) => {
const atIdx = id.indexOf('@');
if (atIdx > 0) {
const local = id.slice(0, atIdx);
const domain = id.slice(atIdx);
const keep = Math.min(2, local.length);
return local.slice(0, keep) + '***' + domain;
}
if (id.length <= 3)
return id;
return id.slice(0, 2) + '***' + id.slice(-1);
};
try {
const redacted = await credentialsService.decrypt(credential, false);
if (typeof redacted.accountIdentifier === 'string' && redacted.accountIdentifier) {
return { accountIdentifier: mask(redacted.accountIdentifier) };
}
for (const key of ['email', 'user', 'username', 'account', 'serviceAccountEmail']) {
const value = redacted[key];
if (typeof value === 'string' && value) {
return { accountIdentifier: mask(value) };
}
}
const raw = await credentialsService.decrypt(credential, true);
const tokenData = raw.oauthTokenData;
if (tokenData && typeof tokenData === 'object') {
const { OauthService } = await import('../../oauth/oauth.service.js');
const identifier = OauthService.extractAccountIdentifier(tokenData);
if (identifier) {
return { accountIdentifier: mask(identifier) };
}
}
return { accountIdentifier: undefined };
}
catch {
return { accountIdentifier: undefined };
}
},
async isAiGatewayCredentialType(credType) {
const config = await getGatewayConfig();
return config?.credentialTypes.includes(credType) ?? false;
},
async listAiGatewayCredentialTypes() {
const config = await getGatewayConfig();
return config?.credentialTypes ?? [];
},
};
if (!credentialIdAllowlist)
return adapter;
const allowed = new Set(credentialIdAllowlist);
return {
...adapter,
list: async (options) => allowed.size === 0 ? [] : (await adapter.list(options)).filter((c) => allowed.has(c.id)),
};
}
createEvaluationConfigAdapter(evaluationConfigService, user) {
const { workflowFinderService, credentialsFinderService, llmJudgeProviderRegistry } = this;
const assertNotReadOnly = () => this.assertInstanceNotReadOnly('evaluations');
const findWorkflow = async (workflowId, scope) => {
const workflow = await workflowFinderService.findWorkflowForUser(workflowId, user, [scope]);
if (!workflow) {
throw new Error(`Workflow ${workflowId} not found or not accessible`);
}
return workflow;
};
const resolveProviders = async (input) => await resolveMetricProviders(input, {
user,
credentialsFinderService,
llmJudgeProviderRegistry,
});
return {
async list(workflowId) {
await findWorkflow(workflowId, 'workflow:read');
const configs = await evaluationConfigService.list(workflowId);
return configs.map(evaluationConfigToSummary);
},
async get(workflowId, configId) {
await findWorkflow(workflowId, 'workflow:read');
const config = await evaluationConfigService.get(workflowId, configId);
return config ? evaluationConfigToSummary(config) : null;
},
async describe(workflowId, configId) {
await findWorkflow(workflowId, 'workflow:read');
const config = await evaluationConfigService.get(workflowId, configId);
return config ? evaluationConfigToDetail(config) : null;
},
async create(workflowId, input) {
assertNotReadOnly();
const workflow = await findWorkflow(workflowId, 'workflow:update');
const resolved = await resolveProviders(input);
const config = await evaluationConfigService.create(workflowId, workflow, user, buildEvaluationConfigDto(resolved));
return evaluationConfigToSummary(config);
},
async update(workflowId, configId, input) {
assertNotReadOnly();
const workflow = await findWorkflow(workflowId, 'workflow:update');
const resolved = await resolveProviders(input);
const config = await evaluationConfigService.update(workflowId, configId, workflow, user, buildEvaluationConfigDto(resolved));
return evaluationConfigToSummary(config);
},
async delete(workflowId, configId) {
assertNotReadOnly();
await findWorkflow(workflowId, 'workflow:update');
await evaluationConfigService.delete(workflowId, configId);
},
};
}
createDataTableAdapter(user, boundProjectId) {
const { dataTableService, dataTableRepository } = this;
const assertNotReadOnly = () => this.assertInstanceNotReadOnly('data tables');
const { resolveProjectId, resolveBoundProjectId } = this.createProjectScopeHelpers(user, boundProjectId);
const logger = this.logger;
const resolveAccessibleTable = async (scopes, dataTableId, disambiguator) => {
const projectIdFilter = disambiguator?.projectId;
const result = await resolveDataTableByIdOrName(dataTableRepository, logger, dataTableId, {
projectIdFilter,
accessFilter: async (id) => await (0, check_access_1.userHasScopes)(user, scopes, false, { dataTableId: id }),
});
if (result.kind === 'miss') {
throw new Error(`Data table "${dataTableId}" not found`);
}
if (result.kind === 'ambiguous') {
const projectIds = result.candidates.map((c) => c.projectId).join(', ');
throw new Error(`Data table name "${dataTableId}" is ambiguous across accessible projects ` +
`(${projectIds}); pass the UUID or include a \`projectId\` to disambiguate.`);
}
if (projectIdFilter && result.table.projectId !== projectIdFilter) {
throw new Error(`Data table "${dataTableId}" does not belong to project "${projectIdFilter}".`);
}
return result.table;
};
const resolveProjectIdForTable = async (scopes, dataTableId, disambiguator) => {
const table = await resolveAccessibleTable(scopes, dataTableId, disambiguator);
return { projectId: table.projectId, resolvedId: table.id };
};
const resolveTableMeta = async (scopes, dataTableId, disambiguator) => {
const table = await resolveAccessibleTable(scopes, dataTableId, disambiguator);
return { projectId: table.projectId, tableName: table.name, resolvedId: table.id };
};
const referenceScopes = {
read: ['dataTable:read'],
readRow: ['dataTable:readRow'],
writeRow: ['dataTable:writeRow'],
update: ['dataTable:update'],
delete: ['dataTable:delete'],
};
return {
async list(options) {
const projectId = await resolveProjectId(['dataTable:listProject'], options?.projectId);
const { data: tables } = await dataTableService.getManyAndCount({
filter: { projectId },
});
return tables.map((t) => ({
id: t.id,
name: t.name,
projectId,
columns: t.columns.map((c) => ({ id: c.id, name: c.name, type: c.type })),
createdAt: t.createdAt.toISOString(),
updatedAt: t.updatedAt.toISOString(),
}));
},
async create(name, columns) {
assertNotReadOnly();
const projectId = await resolveBoundProjectId(['dataTable:create']);
const result = await dataTableService.createDataTable(projectId, { name, columns });
return {
id: result.id,
name: result.name,
projectId,
columns: result.columns.map((c) => ({ id: c.id, name: c.name, type: c.type })),
createdAt: result.createdAt.toISOString(),
updatedAt: result.updatedAt.toISOString(),
};
},
async delete(dataTableId, options) {
assertNotReadOnly();
const { projectId, resolvedId } = await resolveProjectIdForTable(['dataTable:delete'], dataTableId, options);
await dataTableService.deleteDataTable(resolvedId, projectId);
},
async resolveTableReference(dataTableId, options) {
const { projectId, tableName, resolvedId } = await resolveTableMeta(referenceScopes[options?.permission ?? 'read'], dataTableId, options);
return { id: resolvedId, name: tableName, projectId };
},
async getSchema(dataTableId, options) {
const { projectId, resolvedId } = await resolveProjectIdForTable(['dataTable:read'], dataTableId, options);
const columns = await dataTableService.getColumns(resolvedId, projectId);
return columns.map((c, index) => ({
id: c.id,
name: c.name,
type: c.type,
index,
}));
},
async addColumn(dataTableId, column, options) {
assertNotReadOnly();
const { projectId, resolvedId } = await resolveProjectIdForTable(['dataTable:update'], dataTableId, options);
const result = await dataTableService.addColumn(resolvedId, projectId, column);
return {
id: result.id,
name: result.name,
type: result.type,
index: result.index,
};
},
async deleteColumn(dataTableId, columnId, options) {
assertNotReadOnly();
const { projectId, resolvedId } = await resolveProjectIdForTable(['dataTable:update'], dataTableId, options);
await dataTableService.deleteColumn(resolvedId, projectId, columnId);
},
async renameColumn(dataTableId, columnId, newName, options) {
assertNotReadOnly();
const { projectId, resolvedId } = await resolveProjectIdForTable(['dataTable:update'], dataTableId, options);
await dataTableService.renameColumn(resolvedId, projectId, columnId, {
name: newName,
});
},
async queryRows(dataTableId, options) {
const { projectId, resolvedId } = await resolveProjectIdForTable(['dataTable:readRow'], dataTableId, options);
return await dataTableService.getManyRowsAndCount(resolvedId, projectId, {
take: options?.limit ?? 50,
skip: options?.offset ?? 0,
filter: options?.filter,
});
},
async insertRows(dataTableId, rows, options) {
assertNotReadOnly();
const { projectId, tableName, resolvedId } = await resolveTableMeta(['dataTable:writeRow'], dataTableId, options);
const result = await dataTableService.insertRows(resolvedId, projectId, rows, 'count');
return {
insertedCount: typeof result === 'number' ? result : rows.length,
dataTableId: resolvedId,
tableName,
projectId,
};
},
async updateRows(dataTableId, filter, data, options) {
assertNotReadOnly();
const { projectId, tableName, resolvedId } = await resolveTableMeta(['dataTable:writeRow'], dataTableId, options);
const result = await dataTableService.updateRows(resolvedId, projectId, { filter: filter, data: data }, true);
return {
updatedCount: Array.isArray(result) ? result.length : 0,
dataTableId: resolvedId,
tableName,
projectId,
};
},
async deleteRows(dataTableId, filter, options) {
assertNotReadOnly();
const { projectId, tableName, resolvedId } = await resolveTableMeta(['dataTable:writeRow'], dataTableId, options);
const result = await dataTableService.deleteRows(resolvedId, projectId, { filter: filter }, true);
return {
deletedCount: Array.isArray(result) ? result.length : 0,
dataTableId: resolvedId,
tableName,
projectId,
};
},
};
}
createWebResearchAdapter(user, searchProxyConfig) {
const fetchCache = this.webResearchCache;
const searchCacheRef = this.searchCache;
const settingsService = this.settingsService;
const { outboundHttp, ssrfProtectionService } = this;
const sharedTransport = outboundHttp.transport({
ssrf: this.ssrfProtectionService,
});
const userId = user.id;
let resolvedSearchMethod;
let searchResolved = false;
const lazySearch = async (query, options) => {
if (!searchResolved) {
const config = await settingsService.resolveSearchConfig();
resolvedSearchMethod = this.buildSearchMethod(config.braveApiKey ?? '', config.searxngUrl ?? '', searchCacheRef, searchProxyConfig, userId);
searchResolved = true;
}
if (!resolvedSearchMethod)
return { query, results: [] };
return await resolvedSearchMethod(query, options);
};
return {
search: lazySearch,
async fetchUrl(url, options) {
const cacheKey = `${userId}:${url}`;
const cached = fetchCache.get(cacheKey);
if (cached) {
if (options?.authorizeUrl && cached.finalUrl) {
const origHost = new URL(url).hostname;
const finalHost = new URL(cached.finalUrl).hostname;
if (origHost !== finalHost) {
await options.authorizeUrl(cached.finalUrl);
}
}
return cached;
}
const authorizeUrl = options?.authorizeUrl;
const transport = authorizeUrl
? outboundHttp.transport({
ssrf: ssrfProtectionService,
authorize: async (target) => await authorizeUrl(target.href),
})
: sharedTransport;
const page = await (0, web_research_1.fetchAndExtract)(url, {
maxContentLength: options?.maxContentLength,
maxResponseBytes: options?.maxResponseBytes,
timeoutMs: options?.timeoutMs,
abortSignal: options?.abortSignal,
transport,
});
const result = await (0, web_research_1.maybeSummarize)(page);
fetchCache.set(cacheKey, result);
return result;
},
};
}
buildSearchMethod(apiKey, searxngUrl, cache, searchProxyConfig, userId) {
const keyPrefix = userId ? `${userId}:` : '';
const searchCacheKey = (query, options) => {
const { abortSignal: _abortSignal, ...cacheable } = options ?? {};
return `${keyPrefix}${JSON.stringify([query, cacheable])}`;
};
if (searchProxyConfig) {
return async (query, options) => {
const cacheKey = searchCacheKey(query, options);
const cached = cache.get(cacheKey);
if (cached)
return cached;
const result = await (0, ai_utilities_1.braveSearch)('', query, {
...options,
proxyConfig: searchProxyConfig,
});
cache.set(cacheKey, result);
return result;
};
}
if (apiKey) {
return async (query, options) => {
const cacheKey = searchCacheKey(query, options);
const cached = cache.get(cacheKey);
if (cached)
return cached;
const result = await (0, ai_utilities_1.braveSearch)(apiKey, query, options ?? {});
cache.set(cacheKey, result);
return result;
};
}
if (searxngUrl) {
return async (query, options) => {
const cacheKey = searchCacheKey(query, options);
const cached = cache.get(cacheKey);
if (cached)
return cached;
const result = await (0, ai_utilities_1.searxngSearch)(searxngUrl, query, options ?? {});
cache.set(cacheKey, result);
return result;
};
}
return undefined;
}
getNodeDefinitionDirs() {
const catalogDirs = this.nodeCatalogService?.getNodeDefinitionDirs();
if (catalogDirs?.length)
return catalogDirs;
if (!this._nodeDefinitionDirs) {
this._nodeDefinitionDirs = (0, node_definition_resolver_1.resolveBuiltinNodeDefinitionDirs)();
}
return this._nodeDefinitionDirs;
}
getNodeCatalogService() {
return this.nodeCatalogService ?? di_1.Container.get(node_catalog_1.NodeCatalogService);
}
createNodeAdapter(user) {
const getNodes = async () => await this.getNodesFromCache();
const getGatewayConfig = async () => await this.getGatewayConfigOrNull();
const buildMeta = (config, nodeName) => this.buildAiGatewayNodeMeta(config, nodeName);
const findNodeByVersion = (nodes, nodeType, version) => {
if (version !== undefined) {
const exact = nodes.find((n) => {
if (n.name !== nodeType)
return false;
if (Array.isArray(n.version))
return n.version.includes(version);
return n.version === version;
});
if (exact)
return exact;
}
return nodes.find((n) => n.name === nodeType);
};
return {
async listAvailable(options) {
const [nodes, gatewayConfig] = await Promise.all([
getNodes(),
options?.n8nConnectOnly ? getGatewayConfig() : Promise.resolve(null),
]);
let filtered = nodes;
if (options?.query) {
const q = options.query.toLowerCase();
filtered = filtered.filter((n) => n.displayName.toLowerCase().includes(q) ||
n.name.toLowerCase().includes(q) ||
n.description?.toLowerCase().includes(q));
}
const summaries = filtered.map((n) => {
const summary = {
name: n.name,
displayName: n.displayName,
description: n.description ?? '',
group: n.group ?? [],
version: Array.isArray(n.version) ? n.version[n.version.length - 1] : n.version,
};
const meta = buildMeta(gatewayConfig, n.name);
if (meta)
summary.aiGateway = meta;
return summary;
});
return options?.n8nConnectOnly ? summaries.filter((s) => s.aiGateway) : summaries;
},
async listSearchable() {
const [nodes, gatewayConfig] = await Promise.all([getNodes(), getGatewayConfig()]);
const toStringArray = (value) => {
if (typeof value === 'string')
return value;
return value.map((v) => (typeof v === 'string' ? v : v.type));
};
return nodes.map((n) => {
const result = {
name: n.name,
displayName: n.displayName,
description: n.description ?? '',
version: n.version,
inputs: toStringArray(n.inputs),
outputs: toStringArray(n.outputs),
};
const meta = buildMeta(gatewayConfig, n.name);
if (meta)
result.aiGateway = meta;
if (n.codex?.alias) {
result.codex = { alias: n.codex.alias };
}
if (n.builderHint) {
result.builderHint = {};
if (n.builderHint.searchHint) {
result.builderHint.message = n.builderHint.searchHint;
}
if (n.builderHint.inputs) {
const inputs = {};
for (const [key, config] of Object.entries(n.builderHint.inputs)) {
inputs[key] = {
required: config.required,
...(config.displayOptions
? { displayOptions: config.displayOptions }
: {}),
};
}
result.builderHint.inputs = inputs;
}
if (n.builderHint.outputs) {
const outputs = {};
for (const [key, config] of Object.entries(n.builderHint.outputs)) {
outputs[key] = {
...(config.required !== undefined ? { required: config.required } : {}),
...(config.displayOptions
? { displayOptions: config.displayOptions }
: {}),
};
}
result.builderHint.outputs = outputs;
}
}
return result;
});
},
async getDescription(nodeType, version) {
const [nodes, gatewayConfig] = await Promise.all([getNodes(), getGatewayConfig()]);
let desc = version !== undefined
? nodes.find((n) => {
if (n.name !== nodeType)
return false;
if (Array.isArray(n.version))
return n.version.includes(version);
return n.version === version;
})
: undefined;
if (!desc) {
desc = nodes.find((n) => n.name === nodeType);
}
if (!desc) {
throw new Error(`Node type ${nodeType} not found`);
}
const meta = buildMeta(gatewayConfig, desc.name);
return {
name: desc.name,
displayName: desc.displayName,
description: desc.description ?? '',
group: desc.group ?? [],
version: Array.isArray(desc.version)
? desc.version[desc.version.length - 1]
: desc.version,
properties: desc.properties.map((p) => ({
displayName: p.displayName,
name: p.name,
type: p.type,
required: p.required,
description: p.description,
default: p.default,
options: p.options
?.filter((o) => typeof o === 'object' && o !== null && 'name' in o && 'value' in o)
.map((o) => ({
name: String(o.name),
value: o.value,
})),
})),
credentials: desc.credentials?.map((c) => ({
name: c.name,
required: c.required,
...(c.displayOptions
? { displayOptions: c.displayOptions }
: {}),
})),
inputs: Array.isArray(desc.inputs) ? desc.inputs.map(String) : [],
outputs: Array.isArray(desc.outputs) ? desc.outputs.map(String) : [],
...(desc.webhooks ? { webhooks: desc.webhooks } : {}),
...(desc.polling ? { polling: desc.polling } : {}),
...(desc.triggerPanel !== undefined ? { triggerPanel: desc.triggerPanel } : {}),
...(meta ? { aiGateway: meta } : {}),
};
},
getNodeTypeDefinition: async (nodeType, options) => {
const nodeCatalogService = this.getNodeCatalogService();
await nodeCatalogService.initialize();
const { version, resource, operation, mode } = options ?? {};
const getDefinition = async (nodeId) => await nodeCatalogService.getNodeTypeDefinition({
nodeId,
...(version ? { version } : {}),
...(resource ? { resource } : {}),
...(operation ? { operation } : {}),
...(mode ? { mode } : {}),
});
const result = await getDefinition(nodeType);
if (!result.error || nodeType.includes('.'))
return result;
return await getDefinition(`${node_description_transform_1.MCP_REGISTRY_PACKAGE_NAME}.${nodeType}`);
},
listDiscriminators: async (nodeType) => {
const nodeCatalogService = this.getNodeCatalogService();
await nodeCatalogService.initialize();
return (0, node_definition_resolver_1.listNodeDiscriminators)(nodeType, nodeCatalogService.getNodeDefinitionDirs());
},
getParameterIssues: async (nodeType, typeVersion, parameters) => {
const nodes = await getNodes();
const desc = findNodeByVersion(nodes, nodeType, typeVersion);
if (!desc)
return {};
const nodeProperties = desc.properties;
const paramsWithDefaults = resolveDisplayedDefaults(nodeProperties, parameters, nodeType, typeVersion, desc);
const minimalNode = {
id: '',
name: '',
type: nodeType,
typeVersion,
parameters: paramsWithDefaults,
position: [0, 0],
};
const issues = n8n_workflow_1.NodeHelpers.getNodeParametersIssues(nodeProperties, minimalNode, desc);
const allIssues = issues?.parameters ?? {};
const topLevelPropsByName = new Map();
for (const prop of nodeProperties) {
const existing = topLevelPropsByName.get(prop.name);
if (existing) {
existing.push(prop);
}
else {
topLevelPropsByName.set(prop.name, [prop]);
}
}
const filteredIssues = {};
for (const [key, value] of Object.entries(allIssues)) {
const props = topLevelPropsByName.get(key);
if (!props)
continue;
const isDisplayed = props.some((prop) => {
if (prop.type === 'hidden')
return false;
if (prop.displayOptions &&
!n8n_workflow_1.NodeHelpers.displayParameter(paramsWithDefaults, prop, minimalNode, desc)) {
return false;
}
return true;
});
if (!isDisplayed)
continue;
filteredIssues[key] = value;
}
return filteredIssues;
},
getNodeCredentialTypes: async (nodeType, typeVersion, parameters, _existingCredentials) => {
const nodes = await getNodes();
const desc = findNodeByVersion(nodes, nodeType, typeVersion);
if (!desc)
return [];
const credentialTypes = new Set();
const paramsWithDefaults = resolveDisplayedDefaults(desc.properties, parameters, nodeType, typeVersion, desc);
const minimalNode = {
id: '',
name: '',
type: nodeType,
typeVersion,
parameters: paramsWithDefaults,
position: [0, 0],
};
const nodeCredentials = desc.credentials ?? [];
for (const cred of nodeCredentials) {
if (cred.displayOptions) {
if (!n8n_workflow_1.NodeHelpers.displayParameter(paramsWithDefaults, cred, minimalNode, desc)) {
continue;
}
}
credentialTypes.add(cred.name);
}
const issues = n8n_workflow_1.NodeHelpers.getNodeParametersIssues(desc.properties, minimalNode, desc);
const credentialIssues = issues?.credentials ?? {};
for (const credType of Object.keys(credentialIssues)) {
credentialTypes.add(credType);
}
if (parameters.authentication === 'genericCredentialType' && parameters.genericAuthType) {
credentialTypes.add(parameters.genericAuthType);
}
else if (parameters.authentication === 'predefinedCredentialType' &&
parameters.nodeCredentialType) {
credentialTypes.add(parameters.nodeCredentialType);
}
return Array.from(credentialTypes);
},
getResolvedNodeInputs: async (workflowJson, nodeName) => {
const nodeJson = workflowJson.nodes.find((n) => n.name === nodeName);
if (!nodeJson)
return [];
const nodeType = this.nodeTypes.getByNameAndVersion(nodeJson.type, nodeJson.typeVersion ?? 1);
if (!nodeType)
return [];
const workflow = new n8n_workflow_1.Workflow({
nodes: workflowJson.nodes,
connections: workflowJson.connections,
active: false,
nodeTypes: this.nodeTypes,
});
const workflowNode = workflow.getNode(nodeName);
if (!workflowNode)
return [];
await workflow.expression.acquireIsolate();
try {
return n8n_workflow_1.NodeHelpers.getNodeInputs(workflow, workflowNode, nodeType.description);
}
finally {
await workflow.expression.releaseIsolate();
}
},
exploreResources: async (params) => await this.nodeResourceExplorerService.exploreResources(user, params),
};
}
createWorkspaceAdapter(user) {
const { projectService, folderService, tagService, workflowFinderService, workflowService, executionRepository, executionPersistence, eventService, } = this;
const assertNotReadOnly = (resource) => this.assertInstanceNotReadOnly(resource);
const { assertProjectScope } = this.createProjectScopeHelpers(user);
const adapter = {
async getProject(projectId) {
const project = await projectService.getProjectWithScope(user, projectId, ['project:read']);
if (!project)
return null;
return { id: project.id, name: project.name, type: project.type };
},
async listProjects() {
const projects = await projectService.getAccessibleProjects(user);
return projects.map((p) => ({
id: p.id,
name: p.name,
type: p.type,
}));
},
...(this.license.isLicensed('feat:folders')
? {
async listFolders(projectId) {
await assertProjectScope(['folder:list'], projectId);
const [folders] = await folderService.getManyAndCount(projectId, { take: 100 });
return folders.map((f) => ({
id: f.id,
name: f.name,
parentFolderId: f.parentFolderId,
}));
},
async createFolder(name, projectId, parentFolderId) {
assertNotReadOnly('folders');
await assertProjectScope(['folder:create'], projectId);
const folder = await folderService.createFolder({ name, parentFolderId: parentFolderId ?? undefined }, projectId);
return {
id: folder.id,
name: folder.name,
parentFolderId: folder.parentFolderId ?? null,
};
},
async deleteFolder(folderId, projectId, transferToFolderId) {
assertNotReadOnly('folders');
await assertProjectScope(['folder:delete'], projectId);
await folderService.deleteFolder(user, folderId, projectId, {
transferToFolderId: transferToFolderId ?? undefined,
});
},
async moveWorkflowToFolder(workflowId, folderId) {
assertNotReadOnly('workflows');
const workflow = await workflowFinderService.findWorkflowForUser(workflowId, user, [
'workflow:update',
]);
if (!workflow) {
throw new Error(`Workflow ${workflowId} not found or not accessible`);
}
await workflowService.update(user, workflow, workflowId, {
parentFolderId: folderId,
source: 'n8n-ai',
});
},
}
: {}),
async tagWorkflow(workflowId, tagNames) {
assertNotReadOnly('workflows');
const workflow = await workflowFinderService.findWorkflowForUser(workflowId, user, [
'workflow:update',
]);
if (!workflow) {
throw new Error(`Workflow ${workflowId} not found or not accessible`);
}
if (!(0, permissions_1.hasGlobalScope)(user, 'tag:list')) {
throw new Error('User does not have permission to list tags');
}
const existingTags = await tagService.getAll();
const tagMap = new Map(existingTags.map((t) => [t.name.toLowerCase(), t]));
const tagIds = [];
for (const tagName of tagNames) {
const existing = tagMap.get(tagName.toLowerCase());
if (existing) {
tagIds.push(existing.id);
}
else {
if (!(0, permissions_1.hasGlobalScope)(user, 'tag:create')) {
throw new Error('User does not have permission to create tags');
}
const entity = tagService.toEntity({ name: tagName });
const saved = await tagService.save(entity, 'create');
tagIds.push(saved.id);
}
}
await workflowService.update(user, workflow, workflowId, { tagIds, source: 'n8n-ai' });
return tagNames;
},
async listTags() {
if (!(0, permissions_1.hasGlobalScope)(user, 'tag:list')) {
throw new Error('User does not have permission to list tags');
}
const tags = await tagService.getAll();
return tags.map((t) => ({ id: t.id, name: t.name }));
},
async createTag(name) {
if (!(0, permissions_1.hasGlobalScope)(user, 'tag:create')) {
throw new Error('User does not have permission to create tags');
}
const entity = tagService.toEntity({ name });
const saved = await tagService.save(entity, 'create');
return { id: saved.id, name: saved.name };
},
async cleanupTestExecutions(workflowId, options) {
assertNotReadOnly('executions');
const workflow = await workflowFinderService.findWorkflowForUser(workflowId, user, [
'workflow:execute',
]);
if (!workflow) {
throw new Error(`Workflow ${workflowId} not found or not accessible`);
}
const olderThanHours = options?.olderThanHours ?? 1;
const cutoff = new Date(Date.now() - olderThanHours * 60 * 60 * 1000);
const executions = await executionRepository.find({
select: ['id'],
where: {
workflowId,
mode: 'manual',
startedAt: (0, typeorm_1.LessThan)(cutoff),
},
});
if (executions.length === 0) {
return { deletedCount: 0 };
}
const ids = executions.map((e) => e.id);
await executionPersistence.hardDeleteBy({
filters: { workflowId, mode: 'manual' },
accessibleWorkflowIds: [workflowId],
deleteConditions: { deleteBefore: cutoff },
});
eventService.emit('execution-deleted', {
user: {
id: user.id,
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
role: user.role,
},
executionIds: ids,
deleteBefore: cutoff,
});
return { deletedCount: ids.length };
},
};
return adapter;
}
};
exports.InstanceAiAdapterService = InstanceAiAdapterService;
exports.InstanceAiAdapterService = InstanceAiAdapterService = __decorate([
(0, di_1.Service)(),
__metadata("design:paramtypes", [backend_common_1.Logger, config_1.GlobalConfig, workflow_service_1.WorkflowService, workflow_finder_service_1.WorkflowFinderService, db_1.WorkflowRepository, db_1.SharedWorkflowRepository, db_1.ProjectRepository, db_1.ExecutionRepository, credentials_service_1.CredentialsService, credentials_finder_service_1.CredentialsFinderService, active_executions_1.ActiveExecutions, workflow_runner_1.WorkflowRunner, load_nodes_and_credentials_1.LoadNodesAndCredentials, node_types_1.NodeTypes, n8n_core_1.InstanceSettings, data_table_service_1.DataTableService, data_table_repository_1.DataTableRepository, node_resource_explorer_service_1.NodeResourceExplorerService, folder_service_1.FolderService, project_service_ee_1.ProjectService, tag_service_1.TagService, source_control_preferences_service_ee_1.SourceControlPreferencesService, instance_ai_settings_service_1.InstanceAiSettingsService, workflow_history_service_1.WorkflowHistoryService, workflow_service_ee_1.EnterpriseWorkflowService, license_1.License, execution_persistence_1.ExecutionPersistence, event_service_1.EventService, role_service_1.RoleService, telemetry_1.Telemetry, db_1.AiBuilderTemporaryWorkflowRepository, backend_network_1.SsrfProtectionService, backend_network_1.OutboundHttp, ai_gateway_service_1.AiGatewayService, workflow_templates_service_1.WorkflowTemplatesService, node_catalog_1.NodeCatalogService, evaluation_config_service_1.EvaluationConfigService, llm_judge_provider_registry_1.LlmJudgeProviderRegistry])
], InstanceAiAdapterService);
const MAX_RESULT_CHARS = 20_000;
const MAX_NODE_OUTPUT_CHARS = 1_000;
async function resolveMetricProviders(input, deps) {
const { user, credentialsFinderService, llmJudgeProviderRegistry } = deps;
const metrics = await Promise.all(input.metrics.map(async (metric) => {
if (metric.provider)
return metric;
if (!llmJudgeProviderRegistry) {
throw new n8n_workflow_1.UnexpectedError('Cannot derive the judge provider: provider registry is unavailable.');
}
const credential = await credentialsFinderService.findCredentialForUser(metric.credentialId, user, ['credential:read']);
if (!credential) {
throw new n8n_workflow_1.UserError(`Credential "${metric.credentialId}" for metric "${metric.name}" was not found or is not accessible.`);
}
const provider = llmJudgeProviderRegistry.getByCredentialType(credential.type);
if (!provider) {
throw new n8n_workflow_1.UserError(`Credential type "${credential.type}" for metric "${metric.name}" is not a supported LLM judge provider.`);
}
return { ...metric, provider: provider.nodeType };
}));
return { ...input, metrics };
}
function buildEvaluationConfigDto(input) {
return api_types_1.upsertEvaluationConfigSchema.parse({
name: input.name,
startNodeName: input.startNodeName,
endNodeName: input.endNodeName,
datasetSource: 'data_table',
datasetRef: { dataTableId: input.dataTableId },
metrics: input.metrics.map((metric) => ({
id: (0, nanoid_1.nanoid)(),
name: metric.name,
type: 'llm_judge',
config: {
preset: metric.preset,
...(metric.prompt ? { prompt: metric.prompt } : {}),
provider: metric.provider,
credentialId: metric.credentialId,
model: metric.model,
outputType: metric.outputType,
inputs: {
actualAnswer: metric.actualAnswer,
...(metric.userQuery ? { userQuery: metric.userQuery } : {}),
...(metric.expectedAnswer ? { expectedAnswer: metric.expectedAnswer } : {}),
},
},
})),
});
}
function evaluationConfigToSummary(config) {
const dataTableId = config.datasetSource === 'data_table' && 'dataTableId' in config.datasetRef
? config.datasetRef.dataTableId
: undefined;
return {
id: config.id,
workflowId: config.workflowId,
name: config.name,
status: config.status,
invalidReason: config.invalidReason,
startNodeName: config.startNodeName,
endNodeName: config.endNodeName,
metrics: config.metrics.map((metric) => ({
id: metric.id,
name: metric.name,
type: metric.type,
})),
datasetSource: config.datasetSource,
...(dataTableId !== undefined ? { dataTableId } : {}),
};
}
function evaluationConfigToDetail(config) {
const dataTableId = config.datasetSource === 'data_table' && 'dataTableId' in config.datasetRef
? config.datasetRef.dataTableId
: undefined;
return {
id: config.id,
workflowId: config.workflowId,
name: config.name,
status: config.status,
invalidReason: config.invalidReason,
startNodeName: config.startNodeName,
endNodeName: config.endNodeName,
metrics: config.metrics,
datasetSource: config.datasetSource,
...(dataTableId !== undefined ? { dataTableId } : {}),
};
}
async function resolveDataTableByIdOrName(repository, logger, idOrName, options) {
const byId = await repository.findOneBy({ id: idOrName });
if (byId) {
if (options?.accessFilter && !(await options.accessFilter(byId.id))) {
return { kind: 'miss' };
}
return { kind: 'hit', table: byId };
}
const candidates = await repository.findBy({
name: idOrName,
...(options?.projectIdFilter ? { projectId: options.projectIdFilter } : {}),
});
let filtered = candidates;
if (options?.accessFilter) {
filtered = [];
for (const c of candidates) {
if (await options.accessFilter(c.id))
filtered.push(c);
}
}
if (filtered.length === 0)
return { kind: 'miss' };
if (filtered.length > 1)
return { kind: 'ambiguous', candidates: filtered };
const hit = filtered[0];
logger.warn('data-tables tool called with table name instead of id — resolved by name fallback', {
passedValue: idOrName,
resolvedId: hit.id,
projectId: hit.projectId,
});
return { kind: 'hit', table: hit };
}
function truncateResultData(resultData) {
const serialized = JSON.stringify(resultData);
if (serialized.length <= MAX_RESULT_CHARS)
return resultData;
const truncated = {};
for (const [nodeName, items] of Object.entries(resultData)) {
if (!Array.isArray(items) || items.length === 0) {
truncated[nodeName] = items;
continue;
}
const itemStr = JSON.stringify(items[0]);
const preview = itemStr.length > MAX_NODE_OUTPUT_CHARS
? `${itemStr.slice(0, MAX_NODE_OUTPUT_CHARS)}…`
: items[0];
truncated[nodeName] = {
_itemCount: items.length,
_truncated: true,
_firstItemPreview: preview,
};
}
return truncated;
}
function wrapResultDataEntries(data) {
const wrapped = {};
for (const [nodeName, value] of Object.entries(data)) {
wrapped[nodeName] = (0, instance_ai_1.wrapUntrustedData)(JSON.stringify(value, null, 2), 'execution-output', `node:${nodeName}`);
}
return wrapped;
}
const MAX_NODE_ERRORS = 10;
function isFailedNodeRun(nodeRun) {
return (nodeRun.executionStatus === 'error' ||
nodeRun.error !== undefined ||
nodeRun.redactedError !== undefined);
}
function nodeContinuesOnError(node) {
return (node?.continueOnFail === true ||
node?.onError === 'continueRegularOutput' ||
node?.onError === 'continueErrorOutput');
}
function extractNodeErrors(runData, includeUpstreamDetails, workflowNodes = []) {
if (!runData)
return [];
const nodesByName = new Map(workflowNodes.map((node) => [node.name, node]));
const nodeErrors = [];
for (const [nodeName, nodeRuns] of Object.entries(runData)) {
if (nodeErrors.length >= MAX_NODE_ERRORS)
break;
if (nodeContinuesOnError(nodesByName.get(nodeName)))
continue;
const failedRun = nodeRuns.find(isFailedNodeRun);
if (!failedRun)
continue;
const message = failedRun.error
? formatExecutionError(failedRun.error, includeUpstreamDetails)
: failedRun.redactedError
? `${failedRun.redactedError.type} error${failedRun.redactedError.httpCode ? ` (${failedRun.redactedError.httpCode})` : ''}`
: undefined;
nodeErrors.push({
nodeName,
...(message ? { message } : {}),
});
}
return nodeErrors;
}
async function extractExecutionResult(executionId, includeOutputData = true) {
const execution = await di_1.Container.get(execution_persistence_1.ExecutionPersistence).findSingleExecution(executionId, {
includeData: true,
unflattenData: true,
});
if (!execution) {
return { executionId, status: 'unknown' };
}
const status = execution.status === 'error' || execution.status === 'crashed'
? 'error'
: execution.status === 'running' || execution.status === 'new'
? 'running'
: execution.status === 'waiting'
? 'waiting'
: 'success';
const resultData = {};
const runData = execution.data?.resultData?.runData;
const executedNodeNames = Object.keys(runData ?? {});
if (includeOutputData) {
if (runData) {
for (const [nodeName, nodeRuns] of Object.entries(runData)) {
const lastRun = nodeRuns[nodeRuns.length - 1];
if (lastRun?.data?.main) {
const outputItems = lastRun.data.main
.flat()
.filter((item) => item !== null && item !== undefined)
.map((item) => item.json);
if (outputItems.length > 0) {
resultData[nodeName] = truncateNodeOutput(outputItems);
}
}
}
}
}
const error = execution.data?.resultData?.error;
const errorMessage = error ? formatExecutionError(error, includeOutputData) : undefined;
const nodeErrors = extractNodeErrors(runData, includeOutputData, execution.workflowData?.nodes);
return {
executionId,
status,
data: Object.keys(resultData).length > 0
? wrapResultDataEntries(truncateResultData(resultData))
: undefined,
executedNodeNames: executedNodeNames.length > 0 ? executedNodeNames : undefined,
nodeErrors: nodeErrors.length > 0 ? nodeErrors : undefined,
lastNodeExecuted: execution.data?.resultData?.lastNodeExecuted,
error: errorMessage,
startedAt: execution.startedAt?.toISOString(),
finishedAt: execution.stoppedAt?.toISOString(),
};
}
const MAX_ERROR_CHARS = 4_000;
function formatExecutionError(error, includeUpstreamDetails) {
const parts = [];
if (error.message)
parts.push(error.message);
if (includeUpstreamDetails) {
if (error.description && error.description !== error.message) {
parts.push(error.description);
}
if ('messages' in error && error.messages.length > 0) {
parts.push(`Details: ${error.messages.join(' | ')}`);
}
}
else {
const hasDescription = !!error.description && error.description !== error.message;
const hasMessages = 'messages' in error && error.messages.length > 0;
if (hasDescription || hasMessages) {
parts.push('(upstream error details suppressed by the instance AI privacy setting; ask the user to share the node error from the UI)');
}
}
const combined = parts.join(' — ') || 'Unknown error';
return combined.length > MAX_ERROR_CHARS ? `${combined.slice(0, MAX_ERROR_CHARS)}…` : combined;
}
const MAX_NODE_OUTPUT_BYTES = 5_000;
function truncateNodeOutput(items) {
const serialized = JSON.stringify(items);
if (serialized.length <= MAX_NODE_OUTPUT_BYTES)
return items;
const truncated = [];
let size = 2;
for (const item of items) {
const itemStr = JSON.stringify(item);
if (size + itemStr.length + 2 > MAX_NODE_OUTPUT_BYTES)
break;
truncated.push(item);
size += itemStr.length + 1;
}
return {
items: truncated,
truncated: true,
totalItems: items.length,
shownItems: truncated.length,
message: `Output truncated: showing ${truncated.length} of ${items.length} items. Use get-node-output to retrieve full data for this node.`,
};
}
const MAX_ITEM_CHARS = 50_000;
async function extractNodeOutput(executionId, nodeName, options) {
const execution = await di_1.Container.get(execution_persistence_1.ExecutionPersistence).findSingleExecution(executionId, {
includeData: true,
unflattenData: true,
});
if (!execution) {
throw new Error(`Execution ${executionId} not found`);
}
const runData = execution.data?.resultData?.runData;
if (!runData?.[nodeName]) {
throw new Error(`Node "${nodeName}" not found in execution ${executionId}`);
}
const nodeRuns = runData[nodeName];
const lastRun = nodeRuns[nodeRuns.length - 1];
const startIndex = options?.startIndex ?? 0;
const maxItems = Math.min(options?.maxItems ?? 10, 50);
let index = 0;
let totalItems = 0;
const collected = [];
for (const output of lastRun?.data?.main ?? []) {
for (const item of output ?? []) {
totalItems++;
if (index >= startIndex && collected.length < maxItems) {
collected.push(item.json);
}
index++;
}
}
const capped = collected.map((item) => {
const str = JSON.stringify(item);
if (str.length > MAX_ITEM_CHARS) {
return {
_truncatedItem: true,
preview: str.slice(0, MAX_ITEM_CHARS),
originalLength: str.length,
};
}
return item;
});
return {
nodeName,
items: capped.map((item, i) => (0, instance_ai_1.wrapUntrustedData)(JSON.stringify(item, null, 2), 'execution-output', `node:${nodeName}[${startIndex + i}]`)),
totalItems,
returned: { from: startIndex, to: startIndex + capped.length },
};
}
const KNOWN_TRIGGER_TYPES = new Set([
n8n_workflow_1.CHAT_TRIGGER_NODE_TYPE,
n8n_workflow_1.FORM_TRIGGER_NODE_TYPE,
n8n_workflow_1.WEBHOOK_NODE_TYPE,
n8n_workflow_1.SCHEDULE_TRIGGER_NODE_TYPE,
]);
function findTriggerNode(nodes) {
const known = nodes.find((n) => KNOWN_TRIGGER_TYPES.has(n.type));
if (known)
return known;
return nodes.find((n) => (0, n8n_workflow_1.isTriggerNodeType)(n.type));
}
function getExecutionModeForTrigger(node) {
switch (node.type) {
case n8n_workflow_1.WEBHOOK_NODE_TYPE:
return 'webhook';
case n8n_workflow_1.CHAT_TRIGGER_NODE_TYPE:
return 'chat';
case n8n_workflow_1.FORM_TRIGGER_NODE_TYPE:
case n8n_workflow_1.SCHEDULE_TRIGGER_NODE_TYPE:
return 'trigger';
default:
return 'manual';
}
}
async function extractExecutionDebugInfo(executionId, includeOutputData = true, nodeTypes) {
const execution = await di_1.Container.get(execution_persistence_1.ExecutionPersistence).findSingleExecution(executionId, {
includeData: true,
unflattenData: true,
});
if (!execution) {
return {
executionId,
status: 'unknown',
nodeTrace: [],
};
}
const baseResult = await extractExecutionResult(executionId, includeOutputData);
const runData = execution.data?.resultData?.runData;
const nodeTrace = [];
let failedNode;
let failedItemIndex;
let failedRunIndex;
if (runData) {
const workflowNodes = execution.workflowData?.nodes ?? [];
const nodeTypeMap = new Map(workflowNodes.map((n) => [n.name, n.type]));
for (const [nodeName, nodeRuns] of Object.entries(runData)) {
const lastRun = nodeRuns[nodeRuns.length - 1];
if (!lastRun)
continue;
const nodeType = nodeTypeMap.get(nodeName) ?? 'unknown';
nodeTrace.push({
name: nodeName,
type: nodeType,
status: isFailedNodeRun(lastRun) ? 'error' : 'success',
startedAt: lastRun.startTime !== undefined ? new Date(lastRun.startTime).toISOString() : undefined,
finishedAt: lastRun.startTime !== undefined && lastRun.executionTime !== undefined
? new Date(lastRun.startTime + lastRun.executionTime).toISOString()
: undefined,
});
if (lastRun.error !== undefined && !failedNode) {
const errorContext = lastRun.error.context;
failedItemIndex =
typeof errorContext?.itemIndex === 'number' ? errorContext.itemIndex : undefined;
failedRunIndex =
typeof errorContext?.runIndex === 'number' ? errorContext.runIndex : nodeRuns.length - 1;
failedNode = {
name: nodeName,
type: nodeType,
error: formatExecutionError(lastRun.error, includeOutputData),
inputData: includeOutputData
? (() => {
const inputItems = lastRun.data?.main
?.flat()
.filter((item) => item !== null && item !== undefined)
.map((item) => item.json);
if (inputItems && inputItems.length > 0) {
const raw = inputItems[0];
return (0, instance_ai_1.wrapUntrustedData)(JSON.stringify(raw, null, 2), 'execution-output', `failed-node-input:${nodeName}`);
}
return undefined;
})()
: undefined,
};
}
}
}
if (failedNode && includeOutputData && nodeTypes) {
try {
const { nodeName: _omitName, suppressed: _omitSuppressed, ...bundle } = await (0, extract_resolved_node_parameters_1.extractResolvedNodeParameters)(nodeTypes, executionId, failedNode.name, {
itemIndex: failedItemIndex,
runIndex: failedRunIndex,
});
failedNode.resolvedParameters = bundle;
}
catch {
}
}
return {
...baseResult,
failedNode,
nodeTrace,
};
}
function sdkNodeGroupsToRuntime(nodeGroups) {
return nodeGroups ?? [];
}
function hasCredentialId(value) {
if (typeof value !== 'object' || value === null)
return false;
if (Reflect.get(value, 'id') === null && Reflect.get(value, '__aiGatewayManaged') === true) {
return true;
}
const id = Reflect.get(value, 'id');
return typeof id === 'string' && id.trim() !== '';
}
function sanitizeCredentialReferencesForSave(nodes) {
return nodes.map((node) => {
if (!node.credentials)
return node;
const credentials = Object.entries(node.credentials).reduce((acc, [type, value]) => {
if (hasCredentialId(value)) {
acc[type] = value;
}
return acc;
}, {});
if (Object.keys(credentials).length === Object.keys(node.credentials).length)
return node;
const sanitized = { ...node };
if (Object.keys(credentials).length > 0) {
sanitized.credentials = credentials;
}
else {
delete sanitized.credentials;
}
return sanitized;
});
}
function toWorkflowJSON(workflow, options) {
const redact = options?.redactParameters ?? false;
const source = options?.graph ?? workflow;
return {
id: workflow.id,
name: workflow.name,
nodes: (source.nodes ?? []).map((n) => ({
id: n.id ?? '',
name: n.name,
type: n.type,
typeVersion: n.typeVersion,
position: n.position,
parameters: redact ? {} : n.parameters,
credentials: n.credentials,
webhookId: n.webhookId,
disabled: n.disabled,
notes: n.notes,
notesInFlow: n.notesInFlow,
executeOnce: n.executeOnce,
retryOnFail: n.retryOnFail,
alwaysOutputData: n.alwaysOutputData,
onError: n.onError,
})),
connections: source.connections,
settings: workflow.settings,
...(source.nodeGroups ? { nodeGroups: source.nodeGroups } : {}),
};
}
function toWorkflowDetail(workflow, options) {
const redact = options?.redactParameters ?? false;
return {
id: workflow.id,
name: workflow.name,
versionId: workflow.versionId,
activeVersionId: workflow.activeVersionId ?? null,
isArchived: workflow.isArchived,
createdAt: workflow.createdAt.toISOString(),
updatedAt: workflow.updatedAt.toISOString(),
nodes: (workflow.nodes ?? []).map((n) => ({
name: n.name,
type: n.type,
typeVersion: n.typeVersion,
parameters: redact ? undefined : n.parameters,
position: n.position,
webhookId: n.webhookId,
})),
connections: workflow.connections,
settings: workflow.settings,
};
}
async function toWorkflowDetailWithChecksum(workflow, options) {
const detail = toWorkflowDetail(workflow, options);
detail.checksum = await (0, n8n_workflow_1.calculateWorkflowChecksum)(workflow);
return detail;
}
//# sourceMappingURL=instance-ai.adapter.service.js.map