deploy
Deploy n8n workflows to an instance, handling import commands with options for specific files, changed workflows, activation, and separate imports.
Instructions
Deploy workflows to n8n instance - handles all n8n import commands internally
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| activate | No | Activate workflows after importing | |
| all | No | Deploy ALL workflows, not just changed ones | |
| path | No | Optional: Deploy specific workflow file. If not provided, deploys all changed workflows | |
| separate | No | Import as separate workflows (not merged) |
Input Schema (JSON Schema)
{
"properties": {
"activate": {
"description": "Activate workflows after importing",
"type": "boolean"
},
"all": {
"description": "Deploy ALL workflows, not just changed ones",
"type": "boolean"
},
"path": {
"description": "Optional: Deploy specific workflow file. If not provided, deploys all changed workflows",
"type": "string"
},
"separate": {
"description": "Import as separate workflows (not merged)",
"type": "boolean"
}
},
"type": "object"
}
Implementation Reference
- src/tools/registry.ts:219-242 (schema)Tool schema definition and registration in the array returned by getToolDefinitions()name: 'deploy', description: 'Deploy workflows to n8n instance - handles all n8n import commands internally', inputSchema: { type: 'object', properties: { path: { type: 'string', description: 'Optional: Deploy specific workflow file. If not provided, deploys all changed workflows', }, all: { type: 'boolean', description: 'Deploy ALL workflows, not just changed ones', }, activate: { type: 'boolean', description: 'Activate workflows after importing', }, separate: { type: 'boolean', description: 'Import as separate workflows (not merged)', }, }, }, },
- src/tools/handler.ts:131-146 (handler)Dispatch handler in ToolHandler.handleTool() that routes 'deploy' calls to N8nManager methods based on input parameterscase 'deploy': const deployPath = args?.path as string; const deployAll = args?.all as boolean; const deployOptions = { activate: args?.activate as boolean, separate: args?.separate as boolean, }; if (deployPath) { return await this.n8nManager.importWorkflow(deployPath, deployOptions); } else if (deployAll) { return await this.n8nManager.deployAllWorkflows(deployOptions); } else { return await this.n8nManager.deployChangedWorkflows(deployOptions); }
- src/n8n/manager.ts:87-228 (handler)Core handler N8nManager.importWorkflow() - compiles single workflow, injects code, uses n8n CLI to import/deploy to n8n instanceasync importWorkflow(workflowPath: string, options: { separate?: boolean; activate?: boolean; } = {}): Promise<any> { try { // Check if n8n is available const n8nAvailable = await this.checkN8nAvailability(); if (!n8nAvailable) { return { content: [ { type: 'text', text: '❌ n8n CLI is not installed!\n\n' + 'To deploy workflows, you need to install n8n:\n' + ' npm install -g n8n\n' + ' or\n' + ' yarn global add n8n\n\n' + 'After installation, run "n8n start" to start the server.', }, ], }; } // Handle different path formats let fullPath: string; // If the path starts with 'workflows/', remove it to avoid doubling if (workflowPath.startsWith('workflows/')) { workflowPath = workflowPath.substring('workflows/'.length); } // If it's an absolute path, use it directly if (path.isAbsolute(workflowPath)) { fullPath = workflowPath; } else { fullPath = path.join(this.workflowsPath, workflowPath); } // Verify file exists await fs.access(fullPath); // Create temporary file with injected content in /tmp const tempPath = `/tmp/mcflow_deploy_${Date.now()}_${path.basename(workflowPath)}`; try { // Compile the workflow (inject external code/prompts) const workflow = await this.compiler.compileWorkflow(fullPath); // Save compiled workflow to dist directory const fileName = path.basename(workflowPath); await this.compiler.saveCompiledWorkflow(fileName, workflow); // Ensure workflow has required fields for n8n if (!workflow.active && workflow.active !== false) { workflow.active = false; // Default to inactive } // Track which nodes had content injected (for reporting) const injected: string[] = []; if (!workflow.settings) { workflow.settings = { executionOrder: 'v1' }; } if (!workflow.connections) { workflow.connections = {}; } // IMPORTANT: Log whether this is an update or create const isUpdate = !!workflow.id; if (workflow.id) { console.error(`Updating existing workflow with ID: ${workflow.id}`); } else { console.error('No workflow ID found - n8n will create a new workflow'); } // Log injection details if (injected.length > 0) { console.error(`Injecting content for nodes: ${injected.join(', ')}`); } // Validate that code nodes have content let emptyCodeNodes = []; if (workflow.nodes) { for (const node of workflow.nodes) { if (node.type === 'n8n-nodes-base.code' && node.parameters) { if ((!node.parameters.jsCode || node.parameters.jsCode === '') && (!node.parameters.pythonCode || node.parameters.pythonCode === '')) { emptyCodeNodes.push(node.name || 'unnamed'); } } } } if (emptyCodeNodes.length > 0) { console.error(`⚠️ WARNING: The following code nodes have empty content: ${emptyCodeNodes.join(', ')}`); console.error('This may cause nodes to appear disconnected in n8n.'); console.error('Make sure to use "mcflow deploy" instead of deploying the workflow file directly.'); } // Write temporary workflow with injected content // n8n expects a single workflow object (not in an array) for file import await fs.writeFile(tempPath, JSON.stringify(workflow, null, 2)); // Build command using temp file // Use --force flag to update existing workflows let command = `n8n import:workflow --input="${tempPath}" --force`; if (options.activate) { command += ' --activate'; } console.error(`Executing: ${command}`); const { stdout, stderr } = await execAsync(command); // Clean up temp file await fs.unlink(tempPath).catch(() => {}); // Check for errors if (this.hasRealError(stderr, stdout)) { throw new Error(stderr); } return { content: [ { type: 'text', text: `✅ Workflow ${isUpdate ? 'updated' : 'deployed'} successfully!\n\n` + `📁 File: ${workflowPath}\n` + `${options.activate ? '▶️ Status: Activated\n' : '⏸️ Status: Inactive\n'}` + `${injected.length > 0 ? `💉 Injected nodes: ${injected.join(', ')}\n` : ''}` + `${options.separate ? '📦 Mode: Separate execution\n' : ''}` + `\n${stdout || 'Deployment completed.'}`, }, ], }; } catch (error: any) { // Always clean up temp file on error await fs.unlink(tempPath).catch(() => {}); throw new Error(`Failed to deploy workflow: ${error.message}`); } } catch (error: any) { throw new Error(`Failed to deploy workflow: ${error.message}`); } }
- src/n8n/manager.ts:233-371 (handler)Handler N8nManager.deployChangedWorkflows() - deploys only workflows that have changed (default behavior)async deployChangedWorkflows(options: { activate?: boolean; separate?: boolean; } = {}): Promise<any> { try { // Get list of changed workflow files from change tracker const changedFiles = await this.changeTracker.getChangedWorkflows(); if (changedFiles.length === 0) { // Show current deployment status const statusDetails = await this.changeTracker.getChangeDetails(); return { content: [ { type: 'text', text: statusDetails, }, ], }; } // Deploy all changed workflows in parallel const deployPromises = changedFiles.map(async (file) => { const fullPath = path.join(this.workflowsPath, file); // Create a temporary file with injected code in /tmp const tempPath = `/tmp/mcflow_deploy_${Date.now()}_${path.basename(file)}`; try { // Compile the workflow (inject external code/prompts) const workflow = await this.compiler.compileWorkflow(fullPath); // Validate that code nodes have content let emptyCodeNodes = []; if (workflow.nodes) { for (const node of workflow.nodes) { if (node.type === 'n8n-nodes-base.code' && node.parameters) { if ((!node.parameters.jsCode || node.parameters.jsCode === '') && (!node.parameters.pythonCode || node.parameters.pythonCode === '')) { emptyCodeNodes.push(node.name || 'unnamed'); } } } } if (emptyCodeNodes.length > 0) { console.error(`⚠️ WARNING in ${path.basename(file)}: Empty code nodes: ${emptyCodeNodes.join(', ')}`); } // Track which nodes had content injected (for reporting) const injected: string[] = []; // Write temporary workflow with injected code // n8n expects a single workflow object (not in an array) for file import await fs.writeFile(tempPath, JSON.stringify(workflow, null, 2)); // Build command for this workflow using temp file // Use --force flag to update existing workflows let command = `n8n import:workflow --input="${tempPath}" --force`; if (options.activate) { command += ' --activate'; } const { stdout, stderr } = await execAsync(command); // Clean up temp file await fs.unlink(tempPath).catch(() => {}); if (this.hasRealError(stderr, stdout)) { return { file: path.basename(file), relativePath: file, status: 'failed', error: stderr }; } // Log success with any warnings if (stderr) { console.error(`Deployed ${path.basename(file)} with warnings: ${stderr}`); } // Log if code was injected if (injected.length > 0) { console.log(`Injected code for nodes: ${injected.join(', ')}`); } // Mark as deployed in change tracker await this.changeTracker.markDeployed(file); return { file: path.basename(file), relativePath: file, status: 'success', output: stdout || stderr }; } catch (error: any) { // Clean up temp file on error await fs.unlink(tempPath).catch(() => {}); console.error(`Failed to deploy ${path.basename(file)}: ${error.message}`); return { file: path.basename(file), relativePath: file, status: 'failed', error: error.message }; } }); // Wait for all deployments to complete const results = await Promise.all(deployPromises); // Format results const successful = results.filter(r => r.status === 'success'); const failed = results.filter(r => r.status === 'failed'); let output = `🚀 Deployed ${successful.length}/${changedFiles.length} workflows\n\n`; if (successful.length > 0) { output += '✅ Successfully deployed:\n'; for (const result of successful) { output += ` • ${result.file}\n`; } } if (failed.length > 0) { output += '\n❌ Failed to deploy:\n'; for (const result of failed) { // Extract meaningful error message const errorMsg = result.error || 'Unknown error'; const shortError = errorMsg.split('\n')[0].substring(0, 100); output += ` • ${result.file}: ${shortError}\n`; // Log full error to console for debugging console.error(`Full error for ${result.file}:`, result.error); } } output += `\n${options.activate ? '▶️ Status: All activated' : '⏸️ Status: Not activated'}`; output += `\n${options.separate ? '📦 Mode: Separate execution' : '📦 Mode: Standard'}`; return { content: [ { type: 'text', text: output, }, ], }; } catch (error: any) { throw new Error(`Failed to deploy changed workflows: ${error.message}`); } }
- src/n8n/manager.ts:376-538 (handler)Handler N8nManager.deployAllWorkflows() - deploys ALL workflows when 'all: true' parameter is providedasync deployAllWorkflows(options: { activate?: boolean; separate?: boolean; } = {}): Promise<any> { try { // Find the flows directory intelligently let flowsPath: string = ''; const possiblePaths = [ path.join(this.workflowsPath, 'flows'), this.workflowsPath ]; for (const testPath of possiblePaths) { try { await fs.access(testPath); const files = await fs.readdir(testPath); if (files.some(f => f.endsWith('.json'))) { flowsPath = testPath; break; } } catch {} } if (!flowsPath) { flowsPath = possiblePaths[0]; // Default to first option } // Get all workflow files (exclude package/config files) const files = await fs.readdir(flowsPath); const workflowFiles = files.filter(f => f.endsWith('.json') && !f.includes('package.json') && !f.includes('workflow_package.json') ); if (workflowFiles.length === 0) { return { content: [ { type: 'text', text: '📭 No workflows found to deploy.', }, ], }; } // Deploy all workflows in parallel const deployPromises = workflowFiles.map(async (file) => { const fullPath = path.join(flowsPath, file); // Create a temporary file for the compiled workflow // Use a simpler name to avoid path issues const tempFileName = `deploy_${Date.now()}_${file.replace(/[^a-z0-9.-]/gi, '_')}`; const tempPath = path.join('/tmp', tempFileName); try { // Compile the workflow (inject external code/prompts) console.error(`Compiling: ${fullPath}`); const workflow = await this.compiler.compileWorkflow(fullPath); // Write compiled workflow to temp file await fs.writeFile(tempPath, JSON.stringify(workflow, null, 2)); console.error(`Written to temp: ${tempPath}`); // Build command for this workflow using the compiled temp file // Use --force flag to update existing workflows let command = `n8n import:workflow --input="${tempPath}" --force`; if (options.activate) { command += ' --activate'; } console.error(`Executing command for ${file}: ${command}`); const { stdout, stderr } = await execAsync(command); // Clean up temp file await fs.unlink(tempPath).catch(() => {}); // Log raw output for debugging console.error(`[${file}] stdout: ${stdout}`); console.error(`[${file}] stderr: ${stderr}`); if (this.hasRealError(stderr, stdout)) { console.error(`[${file}] Detected real error in deployment`); return { file, status: 'failed', error: stderr }; } // Check for success indicators const isSuccess = stdout?.includes('Successfully imported') || stderr?.includes('Successfully imported') || stderr?.includes('Importing'); if (!isSuccess && !stdout && !stderr) { console.error(`[${file}] No output from import command - possible silent failure`); return { file, status: 'failed', error: 'No output from import command' }; } // Log success with any warnings if (stderr && !this.hasRealError(stderr, stdout)) { console.error(`[${file}] Deployed with warnings: ${stderr}`); } else { console.error(`[${file}] Deployed successfully`); } return { file, status: 'success', output: stdout || stderr || 'Import completed' }; } catch (error: any) { // Clean up temp file on error await fs.unlink(tempPath).catch(() => {}); console.error(`Failed to deploy ${file}: ${error.message}`); return { file, status: 'failed', error: error.message }; } }); // Wait for all deployments to complete const results = await Promise.all(deployPromises); // Format results const successful = results.filter(r => r.status === 'success'); const failed = results.filter(r => r.status === 'failed'); console.error(`\n=== Deployment Summary ===`); console.error(`Total workflows: ${workflowFiles.length}`); console.error(`Successful: ${successful.length}`); console.error(`Failed: ${failed.length}`); console.error(`Results:`, JSON.stringify(results, null, 2)); let output = `🚀 Deployed ${successful.length}/${workflowFiles.length} workflows\n\n`; if (successful.length > 0) { output += '✅ Successfully deployed:\n'; for (const result of successful) { output += ` • ${result.file}\n`; } } if (failed.length > 0) { output += '\n❌ Failed to deploy:\n'; for (const result of failed) { // Extract meaningful error message const errorMsg = result.error || 'Unknown error'; const shortError = errorMsg.split('\n')[0].substring(0, 100); output += ` • ${result.file}: ${shortError}\n`; // Log full error to console for debugging console.error(`Full error for ${result.file}:`, result.error); } } output += `\n${options.activate ? '▶️ Status: All activated' : '⏸️ Status: Not activated'}`; output += `\n${options.separate ? '📦 Mode: Separate execution' : '📦 Mode: Standard'}`; return { content: [ { type: 'text', text: output, }, ], }; } catch (error: any) { throw new Error(`Failed to deploy all workflows: ${error.message}`); } }