feat: Add workflow activation/deactivation via diff operations

Implements workflow activation and deactivation as diff operations in
n8n_update_partial_workflow tool, following the pattern of other
configuration operations.

Changes:
- Add activateWorkflow/deactivateWorkflow API methods
- Add operation types to diff engine
- Update tool documentation
- Remove activation limitation

Resolves #399
Credits: ArtemisAI, cmj-hub for investigation and initial implementation
Conceived by Romuald Członkowski - www.aiadvisors.pl/en
This commit is contained in:
czlonkowski
2025-11-06 22:49:46 +01:00
parent 3d5ceae43f
commit 346fa3c8d2
10 changed files with 248 additions and 13 deletions

View File

@@ -1561,7 +1561,6 @@ export async function handleListAvailableTools(context?: InstanceContext): Promi
maxRetries: config.maxRetries
} : null,
limitations: [
'Cannot activate/deactivate workflows via API',
'Cannot execute workflows directly (must use webhooks)',
'Cannot stop running executions',
'Tags and credentials have limited API support'

View File

@@ -245,15 +245,52 @@ export async function handleUpdatePartialWorkflow(
// Update workflow via API
try {
const updatedWorkflow = await client.updateWorkflow(input.id, diffResult.workflow!);
// Handle activation/deactivation if requested
let finalWorkflow = updatedWorkflow;
let activationMessage = '';
if (diffResult.shouldActivate) {
try {
finalWorkflow = await client.activateWorkflow(input.id);
activationMessage = ' Workflow activated.';
} catch (activationError) {
logger.error('Failed to activate workflow after update', activationError);
return {
success: false,
error: 'Workflow updated successfully but activation failed',
details: {
workflowUpdated: true,
activationError: activationError instanceof Error ? activationError.message : 'Unknown error'
}
};
}
} else if (diffResult.shouldDeactivate) {
try {
finalWorkflow = await client.deactivateWorkflow(input.id);
activationMessage = ' Workflow deactivated.';
} catch (deactivationError) {
logger.error('Failed to deactivate workflow after update', deactivationError);
return {
success: false,
error: 'Workflow updated successfully but deactivation failed',
details: {
workflowUpdated: true,
deactivationError: deactivationError instanceof Error ? deactivationError.message : 'Unknown error'
}
};
}
}
return {
success: true,
data: updatedWorkflow,
message: `Workflow "${updatedWorkflow.name}" updated successfully. Applied ${diffResult.operationsApplied} operations.`,
data: finalWorkflow,
message: `Workflow "${finalWorkflow.name}" updated successfully. Applied ${diffResult.operationsApplied} operations.${activationMessage}`,
details: {
operationsApplied: diffResult.operationsApplied,
workflowId: updatedWorkflow.id,
workflowName: updatedWorkflow.name,
workflowId: finalWorkflow.id,
workflowName: finalWorkflow.name,
active: finalWorkflow.active,
applied: diffResult.applied,
failed: diffResult.failed,
errors: diffResult.errors,

View File

@@ -4,7 +4,7 @@ export const n8nUpdatePartialWorkflowDoc: ToolDocumentation = {
name: 'n8n_update_partial_workflow',
category: 'workflow_management',
essentials: {
description: 'Update workflow incrementally with diff operations. Types: addNode, removeNode, updateNode, moveNode, enable/disableNode, addConnection, removeConnection, rewireConnection, cleanStaleConnections, replaceConnections, updateSettings, updateName, add/removeTag. Supports smart parameters (branch, case) for multi-output nodes. Full support for AI connections (ai_languageModel, ai_tool, ai_memory, ai_embedding, ai_vectorStore, ai_document, ai_textSplitter, ai_outputParser).',
description: 'Update workflow incrementally with diff operations. Types: addNode, removeNode, updateNode, moveNode, enable/disableNode, addConnection, removeConnection, rewireConnection, cleanStaleConnections, replaceConnections, updateSettings, updateName, add/removeTag, activateWorkflow, deactivateWorkflow. Supports smart parameters (branch, case) for multi-output nodes. Full support for AI connections (ai_languageModel, ai_tool, ai_memory, ai_embedding, ai_vectorStore, ai_document, ai_textSplitter, ai_outputParser).',
keyParameters: ['id', 'operations', 'continueOnError'],
example: 'n8n_update_partial_workflow({id: "wf_123", operations: [{type: "rewireConnection", source: "IF", from: "Old", to: "New", branch: "true"}]})',
performance: 'Fast (50-200ms)',
@@ -19,11 +19,12 @@ export const n8nUpdatePartialWorkflowDoc: ToolDocumentation = {
'For AI connections, specify sourceOutput type (ai_languageModel, ai_tool, etc.)',
'Batch AI component connections for atomic updates',
'Auto-sanitization: ALL nodes auto-fixed during updates (operator structures, missing metadata)',
'Node renames automatically update all connection references - no manual connection operations needed'
'Node renames automatically update all connection references - no manual connection operations needed',
'Activate/deactivate workflows: Use activateWorkflow/deactivateWorkflow operations (requires activatable triggers like webhook/schedule)'
]
},
full: {
description: `Updates workflows using surgical diff operations instead of full replacement. Supports 15 operation types for precise modifications. Operations are validated and applied atomically by default - all succeed or none are applied.
description: `Updates workflows using surgical diff operations instead of full replacement. Supports 17 operation types for precise modifications. Operations are validated and applied atomically by default - all succeed or none are applied.
## Available Operations:
@@ -48,6 +49,10 @@ export const n8nUpdatePartialWorkflowDoc: ToolDocumentation = {
- **addTag**: Add a workflow tag
- **removeTag**: Remove a workflow tag
### Workflow Activation Operations (2 types):
- **activateWorkflow**: Activate the workflow to enable automatic execution via triggers
- **deactivateWorkflow**: Deactivate the workflow to prevent automatic execution
## Smart Parameters for Multi-Output Nodes
For **IF nodes**, use semantic 'branch' parameter instead of technical sourceIndex:

View File

@@ -170,6 +170,24 @@ export class N8nApiClient {
}
}
async activateWorkflow(id: string): Promise<Workflow> {
try {
const response = await this.client.post(`/workflows/${id}/activate`);
return response.data;
} catch (error) {
throw handleN8nApiError(error);
}
}
async deactivateWorkflow(id: string): Promise<Workflow> {
try {
const response = await this.client.post(`/workflows/${id}/deactivate`);
return response.data;
} catch (error) {
throw handleN8nApiError(error);
}
}
/**
* Lists workflows from n8n instance.
*

View File

@@ -25,6 +25,8 @@ import {
UpdateNameOperation,
AddTagOperation,
RemoveTagOperation,
ActivateWorkflowOperation,
DeactivateWorkflowOperation,
CleanStaleConnectionsOperation,
ReplaceConnectionsOperation
} from '../types/workflow-diff';
@@ -32,6 +34,7 @@ import { Workflow, WorkflowNode, WorkflowConnection } from '../types/n8n-api';
import { Logger } from '../utils/logger';
import { validateWorkflowNode, validateWorkflowConnections } from './n8n-validation';
import { sanitizeNode, sanitizeWorkflowNodes } from './node-sanitizer';
import { isActivatableTrigger } from '../utils/node-type-utils';
const logger = new Logger({ prefix: '[WorkflowDiffEngine]' });
@@ -214,12 +217,23 @@ export class WorkflowDiffEngine {
}
const operationsApplied = request.operations.length;
// Extract activation flags from workflow object
const shouldActivate = (workflowCopy as any)._shouldActivate === true;
const shouldDeactivate = (workflowCopy as any)._shouldDeactivate === true;
// Clean up temporary flags
delete (workflowCopy as any)._shouldActivate;
delete (workflowCopy as any)._shouldDeactivate;
return {
success: true,
workflow: workflowCopy,
operationsApplied,
message: `Successfully applied ${operationsApplied} operations (${nodeOperations.length} node ops, ${otherOperations.length} other ops)`,
warnings: this.warnings.length > 0 ? this.warnings : undefined
warnings: this.warnings.length > 0 ? this.warnings : undefined,
shouldActivate: shouldActivate || undefined,
shouldDeactivate: shouldDeactivate || undefined
};
}
} catch (error) {
@@ -262,6 +276,10 @@ export class WorkflowDiffEngine {
case 'addTag':
case 'removeTag':
return null; // These are always valid
case 'activateWorkflow':
return this.validateActivateWorkflow(workflow, operation);
case 'deactivateWorkflow':
return this.validateDeactivateWorkflow(workflow, operation);
case 'cleanStaleConnections':
return this.validateCleanStaleConnections(workflow, operation);
case 'replaceConnections':
@@ -315,6 +333,12 @@ export class WorkflowDiffEngine {
case 'removeTag':
this.applyRemoveTag(workflow, operation);
break;
case 'activateWorkflow':
this.applyActivateWorkflow(workflow, operation);
break;
case 'deactivateWorkflow':
this.applyDeactivateWorkflow(workflow, operation);
break;
case 'cleanStaleConnections':
this.applyCleanStaleConnections(workflow, operation);
break;
@@ -847,13 +871,46 @@ export class WorkflowDiffEngine {
private applyRemoveTag(workflow: Workflow, operation: RemoveTagOperation): void {
if (!workflow.tags) return;
const index = workflow.tags.indexOf(operation.tag);
if (index !== -1) {
workflow.tags.splice(index, 1);
}
}
// Workflow activation operation validators
private validateActivateWorkflow(workflow: Workflow, operation: ActivateWorkflowOperation): string | null {
// Check if workflow has at least one activatable trigger
// Issue #351: executeWorkflowTrigger cannot activate workflows
const activatableTriggers = workflow.nodes.filter(
node => !node.disabled && isActivatableTrigger(node.type)
);
if (activatableTriggers.length === 0) {
return 'Cannot activate workflow: No activatable trigger nodes found. Workflows must have at least one enabled trigger node (webhook, schedule, email, etc.). Note: executeWorkflowTrigger cannot activate workflows as they can only be invoked by other workflows.';
}
return null;
}
private validateDeactivateWorkflow(workflow: Workflow, operation: DeactivateWorkflowOperation): string | null {
// Deactivation is always valid - any workflow can be deactivated
return null;
}
// Workflow activation operation appliers
private applyActivateWorkflow(workflow: Workflow, operation: ActivateWorkflowOperation): void {
// Set flag in workflow object to indicate activation intent
// The handler will call the API method after workflow update
(workflow as any)._shouldActivate = true;
}
private applyDeactivateWorkflow(workflow: Workflow, operation: DeactivateWorkflowOperation): void {
// Set flag in workflow object to indicate deactivation intent
// The handler will call the API method after workflow update
(workflow as any)._shouldDeactivate = true;
}
// Connection cleanup operation validators
private validateCleanStaleConnections(workflow: Workflow, operation: CleanStaleConnectionsOperation): string | null {
// This operation is always valid - it just cleans up what it finds

View File

@@ -114,6 +114,16 @@ export interface RemoveTagOperation extends DiffOperation {
tag: string;
}
export interface ActivateWorkflowOperation extends DiffOperation {
type: 'activateWorkflow';
// No additional properties needed - just activates the workflow
}
export interface DeactivateWorkflowOperation extends DiffOperation {
type: 'deactivateWorkflow';
// No additional properties needed - just deactivates the workflow
}
// Connection Cleanup Operations
export interface CleanStaleConnectionsOperation extends DiffOperation {
type: 'cleanStaleConnections';
@@ -148,6 +158,8 @@ export type WorkflowDiffOperation =
| UpdateNameOperation
| AddTagOperation
| RemoveTagOperation
| ActivateWorkflowOperation
| DeactivateWorkflowOperation
| CleanStaleConnectionsOperation
| ReplaceConnectionsOperation;
@@ -176,6 +188,8 @@ export interface WorkflowDiffResult {
applied?: number[]; // Indices of successfully applied operations (when continueOnError is true)
failed?: number[]; // Indices of failed operations (when continueOnError is true)
staleConnectionsRemoved?: Array<{ from: string; to: string }>; // For cleanStaleConnections operation
shouldActivate?: boolean; // Flag to activate workflow after update (for activateWorkflow operation)
shouldDeactivate?: boolean; // Flag to deactivate workflow after update (for deactivateWorkflow operation)
}
// Helper type for node reference (supports both ID and name)