n8n
Version:
n8n Workflow Automation Tool
404 lines • 15.8 kB
JavaScript
;
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