UNPKG

n8n

Version:

n8n Workflow Automation Tool

404 lines 15.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.detectTriggerNode = detectTriggerNode; exports.validateCompatibility = validateCompatibility; exports.normalizeTriggerInput = normalizeTriggerInput; exports.inferInputSchema = inferInputSchema; exports.executeWorkflow = executeWorkflow; exports.extractResult = extractResult; exports.resolveWorkflowTool = resolveWorkflowTool; const agents_1 = require("@n8n/agents"); const api_types_1 = require("@n8n/api-types"); const n8n_workflow_1 = require("n8n-workflow"); const zod_1 = require("zod"); const agent_config_composition_1 = require("../json-config/agent-config-composition"); const SUPPORTED_TRIGGERS = { [n8n_workflow_1.MANUAL_TRIGGER_NODE_TYPE]: 'manual', [n8n_workflow_1.EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE]: 'executeWorkflow', [n8n_workflow_1.CHAT_TRIGGER_NODE_TYPE]: 'chat', [n8n_workflow_1.SCHEDULE_TRIGGER_NODE_TYPE]: 'schedule', [n8n_workflow_1.FORM_TRIGGER_NODE_TYPE]: 'form', }; const _assertSupportedTriggersInSync = SUPPORTED_TRIGGERS; void _assertSupportedTriggersInSync; const INCOMPATIBLE_NODE_TYPES = new Set(api_types_1.INCOMPATIBLE_WORKFLOW_TOOL_BODY_NODE_TYPES); const DEFAULT_TIMEOUT_MS = 120_000; const MAX_RESULT_CHARS = 20_000; const MAX_NODE_OUTPUT_BYTES = 5_000; function detectTriggerNode(workflow) { const nodes = workflow.nodes ?? []; for (const node of nodes) { const triggerType = SUPPORTED_TRIGGERS[node.type]; if (triggerType) { return { node, triggerType }; } } throw new Error(`Workflow "${workflow.name}" has no supported trigger node. ` + `Supported triggers: ${Object.keys(SUPPORTED_TRIGGERS).join(', ')}`); } function validateCompatibility(workflow) { const nodes = workflow.nodes ?? []; const incompatible = nodes.filter((n) => INCOMPATIBLE_NODE_TYPES.has(n.type)); if (incompatible.length > 0) { const names = incompatible.map((n) => `${n.name} (${n.type})`).join(', '); throw new Error(`Workflow "${workflow.name}" contains incompatible nodes for agent execution: ${names}`); } } function normalizeTriggerInput(triggerNode, triggerType, inputData) { switch (triggerType) { case 'chat': return { [triggerNode.name]: [ { json: { sessionId: `agent-${Date.now()}`, action: 'sendMessage', chatInput: typeof inputData.message === 'string' ? inputData.message : JSON.stringify(inputData), }, }, ], }; case 'schedule': { const now = new Date(); return { [triggerNode.name]: [ { json: { timestamp: now.toISOString(), 'Readable date': now.toLocaleString(), 'Day of week': now.toLocaleDateString('en-US', { weekday: 'long' }), Year: String(now.getFullYear()), Month: now.toLocaleDateString('en-US', { month: 'long' }), 'Day of month': String(now.getDate()).padStart(2, '0'), Hour: String(now.getHours()).padStart(2, '0'), Minute: String(now.getMinutes()).padStart(2, '0'), Second: String(now.getSeconds()).padStart(2, '0'), }, }, ], }; } default: return { [triggerNode.name]: [{ json: inputData }], }; } } function fieldTypeToZod(type, label) { switch (type) { case 'number': return zod_1.z.number().describe(label); case 'boolean': return zod_1.z.boolean().describe(label); default: return zod_1.z.string().describe(label); } } function schemaFromWorkflowInputs(triggerNode) { const params = triggerNode.parameters ?? {}; const workflowInputs = params.workflowInputs; if (!workflowInputs?.values?.length) return null; const shape = {}; for (const field of workflowInputs.values) { if (!field.name) continue; shape[field.name] = fieldTypeToZod(field.type, field.name); } return Object.keys(shape).length > 0 ? zod_1.z.object(shape) : null; } function schemaFromJsonExample(triggerNode) { const jsonExample = triggerNode.parameters?.jsonExample; if (!jsonExample) return null; let parsed; try { parsed = JSON.parse(jsonExample); } catch { return null; } if (typeof parsed !== 'object' || parsed === null) return null; const shape = {}; for (const [key, value] of Object.entries(parsed)) { shape[key] = fieldTypeToZod(typeof value, key); } return Object.keys(shape).length > 0 ? zod_1.z.object(shape) : null; } function inferInputSchema(triggerNode, triggerType) { switch (triggerType) { case 'chat': return zod_1.z.object({ message: zod_1.z.string() }); case 'manual': return zod_1.z.object({ input: zod_1.z.string().optional() }); case 'schedule': return zod_1.z.object({}); case 'form': return zod_1.z.object({ reason: zod_1.z.string().optional().describe('Why the user should fill out this form'), }); case 'executeWorkflow': return (schemaFromWorkflowInputs(triggerNode) ?? schemaFromJsonExample(triggerNode) ?? zod_1.z.object({}).catchall(zod_1.z.unknown())); default: return zod_1.z.object({}).catchall(zod_1.z.unknown()); } } async function executeWorkflow(workflow, triggerNode, triggerType, inputData, context, allOutputs = false) { const { workflowRunner, activeExecutions, executionRepository } = context; const triggerPinData = normalizeTriggerInput(triggerNode, triggerType, inputData); const workflowPinData = workflow.pinData ?? {}; const mergedPinData = { ...workflowPinData, ...triggerPinData }; const executionMode = triggerType === 'chat' ? 'chat' : triggerType === 'schedule' ? 'trigger' : 'manual'; const runData = { executionMode, workflowData: workflow, userId: context.userId, startNodes: [{ name: triggerNode.name, sourceData: null }], pinData: mergedPinData, executionData: (0, n8n_workflow_1.createRunExecutionData)({ startData: {}, resultData: { pinData: mergedPinData, runData: {} }, executionData: { contextData: {}, metadata: {}, nodeExecutionStack: [ { node: triggerNode, data: { main: [triggerPinData[triggerNode.name]] }, source: null, }, ], waitingExecution: {}, waitingExecutionSource: {}, }, }), }; const executionId = await workflowRunner.run(runData); const timeoutMs = DEFAULT_TIMEOUT_MS; if (activeExecutions.has(executionId)) { let timeoutId; const timeoutPromise = new Promise((_, reject) => { timeoutId = setTimeout(() => { reject(new Error(`Execution timed out after ${timeoutMs}ms`)); }, timeoutMs); }); try { await Promise.race([activeExecutions.getPostExecutePromise(executionId), timeoutPromise]); clearTimeout(timeoutId); } catch (error) { clearTimeout(timeoutId); if (error instanceof Error && error.message.includes('timed out')) { try { activeExecutions.stopExecution(executionId, new n8n_workflow_1.TimeoutExecutionCancelledError(executionId)); } catch { } return { executionId, status: 'error', error: `Execution timed out after ${timeoutMs}ms and was cancelled`, }; } throw error; } } return await extractResult(executionRepository, executionId, allOutputs); } function normaliseExecutionStatus(status) { if (status === 'error' || status === 'crashed') return 'error'; if (status === 'running' || status === 'new') return 'running'; if (status === 'waiting') return 'waiting'; return 'success'; } function outputItemsFromNodeRuns(nodeRuns) { const lastRun = nodeRuns[nodeRuns.length - 1]; if (!lastRun?.data?.main) return []; return lastRun.data.main .flat() .filter((item) => item !== null && item !== undefined) .map((item) => item.json); } function collectResultData(runData, allOutputs) { const resultData = {}; if (allOutputs) { for (const [nodeName, nodeRuns] of Object.entries(runData)) { const outputItems = outputItemsFromNodeRuns(nodeRuns); if (outputItems.length > 0) { resultData[nodeName] = truncateNodeOutput(outputItems); } } return resultData; } const nodeNames = Object.keys(runData); const lastNodeName = nodeNames[nodeNames.length - 1]; if (lastNodeName) { const outputItems = outputItemsFromNodeRuns(runData[lastNodeName]); if (outputItems.length > 0) { resultData[lastNodeName] = truncateNodeOutput(outputItems); } } return resultData; } async function extractResult(executionRepository, executionId, allOutputs) { const execution = await executionRepository.findSingleExecution(executionId, { includeData: true, unflattenData: true, }); if (!execution) { return { executionId, status: 'unknown' }; } const runData = execution.data?.resultData?.runData; const resultData = runData ? collectResultData(runData, allOutputs) : {}; return { executionId, status: normaliseExecutionStatus(execution.status), data: Object.keys(resultData).length > 0 ? truncateResultData(resultData) : undefined, error: execution.data?.resultData?.error?.message, }; } 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.`, }; } function truncateResultData(data) { const serialized = JSON.stringify(data); if (serialized.length <= MAX_RESULT_CHARS) return data; const truncated = {}; for (const [nodeName, rawItems] of Object.entries(data)) { if (!Array.isArray(rawItems) || rawItems.length === 0) { truncated[nodeName] = rawItems; continue; } const items = rawItems; const firstItem = items[0]; const itemStr = JSON.stringify(firstItem); const preview = itemStr.length > 1_000 ? `${itemStr.slice(0, 1_000)}…` : firstItem; truncated[nodeName] = { _itemCount: items.length, _truncated: true, _firstItemPreview: preview, }; } return truncated; } async function resolveWorkflowTool(descriptor, context) { return await buildWorkflowTool(descriptor, context); } async function buildWorkflowTool(descriptor, context) { const { workflowRepository, workflowFinderService, userRepository } = context; const workflowName = descriptor.workflow; const whereClause = { name: workflowName }; if (context.projectId) { whereClause.shared = { projectId: context.projectId }; } const candidateWorkflow = await workflowRepository.findOne({ where: whereClause, relations: ['shared'], }); if (!candidateWorkflow) { throw new Error(`Workflow "${workflowName}" not found`); } const user = await userRepository.findOne({ where: { id: context.userId }, relations: ['role'] }); if (!user) { throw new Error(`User "${context.userId}" not found`); } const workflow = await workflowFinderService.findWorkflowForUser(candidateWorkflow.id, user, [ 'workflow:execute', ]); if (!workflow) { throw new Error(`Workflow "${workflowName}" not found or user does not have execute access`); } validateCompatibility(workflow); const { node: triggerNode, triggerType } = detectTriggerNode(workflow); const toolName = toToolName(descriptor.name ?? workflowName); const toolDescription = descriptor.description ?? `Execute the "${workflowName}" workflow`; const inputSchema = inferInputSchema(triggerNode, triggerType); const allOutputs = descriptor.allOutputs ?? false; if (triggerType === 'form') { const formPath = triggerNode.parameters?.path ?? triggerNode.parameters?.options?.path ?? triggerNode.webhookId ?? workflow.id; const baseUrl = (context.webhookBaseUrl ?? 'http://localhost:5678/').replace(/\/$/, ''); const formUrl = `${baseUrl}/form/${formPath}`; const builder = new agents_1.Tool(toolName) .description(toolDescription === `Execute the "${workflowName}" workflow` ? `Send the user a link to the "${workflowName}" form. The workflow runs automatically when they submit.` : toolDescription) .input(inputSchema) .toMessage(() => ({ type: 'custom', components: [ { type: 'section', text: `📋 *<${formUrl}|Click here to open the form>*` }, ], })) .handler(async (input) => { const reason = input.reason ?? `Please fill out the ${workflowName} form`; return { status: 'form_link_sent', formUrl, message: reason }; }); const built = builder.build(); return { ...built, metadata: { kind: 'workflow', workflowId: workflow.id, workflowName: workflow.name, triggerType, }, }; } const builder = new agents_1.Tool(toolName) .description(toolDescription) .input(inputSchema) .output(zod_1.z.object({ executionId: zod_1.z.string(), status: zod_1.z.string(), data: zod_1.z.record(zod_1.z.unknown()).optional(), error: zod_1.z.string().optional(), })) .handler(async (input) => { return await executeWorkflow(workflow, triggerNode, triggerType, input, context, allOutputs); }); const built = builder.build(); return { ...built, metadata: { kind: 'workflow', workflowId: workflow.id, workflowName: workflow.name, triggerType, }, }; } const toToolName = agent_config_composition_1.sanitizeToolName; //# sourceMappingURL=workflow-tool-factory.js.map