- Added comprehensive N8N development tools collection - Added Docker-containerized mock API server for testing - Added complete documentation and setup guides - Added mock API server with health checks and data endpoints - Tools include workflow analyzers, debuggers, and controllers 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
445 lines
15 KiB
Python
445 lines
15 KiB
Python
"""
|
|
Workflow Controller for N8N
|
|
|
|
This module provides functionality to control N8N workflows - stopping all workflows
|
|
and managing workflow activation states for testing purposes.
|
|
"""
|
|
|
|
import logging
|
|
from typing import List, Dict, Any, Optional
|
|
from .n8n_client import N8NClient
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
class WorkflowController:
|
|
"""Controller for managing N8N workflow states."""
|
|
|
|
def __init__(self, client: Optional[N8NClient] = None):
|
|
"""
|
|
Initialize the workflow controller.
|
|
|
|
Args:
|
|
client: N8N client instance. If None, creates a new one.
|
|
"""
|
|
self.client = client or N8NClient()
|
|
self._original_states = {} # Store original workflow states for restoration
|
|
|
|
def stop_all_workflows(self, exclude_ids: Optional[List[str]] = None) -> Dict[str, Any]:
|
|
"""
|
|
Stop (deactivate) all workflows except those in exclude list.
|
|
|
|
Args:
|
|
exclude_ids: List of workflow IDs to exclude from stopping
|
|
|
|
Returns:
|
|
Summary of stopped workflows
|
|
"""
|
|
exclude_ids = exclude_ids or []
|
|
workflows = self.client.list_workflows()
|
|
|
|
stopped = []
|
|
failed = []
|
|
skipped = []
|
|
|
|
for workflow in workflows:
|
|
workflow_id = workflow.get('id')
|
|
workflow_name = workflow.get('name', 'Unknown')
|
|
is_active = workflow.get('active', False)
|
|
|
|
if workflow_id in exclude_ids:
|
|
skipped.append({
|
|
'id': workflow_id,
|
|
'name': workflow_name,
|
|
'reason': 'excluded'
|
|
})
|
|
continue
|
|
|
|
if not is_active:
|
|
skipped.append({
|
|
'id': workflow_id,
|
|
'name': workflow_name,
|
|
'reason': 'already_inactive'
|
|
})
|
|
continue
|
|
|
|
# Store original state for restoration
|
|
self._original_states[workflow_id] = {
|
|
'active': is_active,
|
|
'name': workflow_name
|
|
}
|
|
|
|
try:
|
|
# Deactivate workflow
|
|
updated_workflow = {
|
|
**workflow,
|
|
'active': False
|
|
}
|
|
self.client.update_workflow(workflow_id, updated_workflow)
|
|
|
|
stopped.append({
|
|
'id': workflow_id,
|
|
'name': workflow_name,
|
|
'was_active': is_active
|
|
})
|
|
logger.info(f"Stopped workflow: {workflow_name} ({workflow_id})")
|
|
|
|
except Exception as e:
|
|
failed.append({
|
|
'id': workflow_id,
|
|
'name': workflow_name,
|
|
'error': str(e)
|
|
})
|
|
logger.error(f"Failed to stop workflow {workflow_name}: {e}")
|
|
|
|
summary = {
|
|
'stopped': stopped,
|
|
'failed': failed,
|
|
'skipped': skipped,
|
|
'total_processed': len(workflows),
|
|
'stopped_count': len(stopped),
|
|
'failed_count': len(failed),
|
|
'skipped_count': len(skipped)
|
|
}
|
|
|
|
logger.info(f"Workflow stop summary: {summary['stopped_count']} stopped, "
|
|
f"{summary['failed_count']} failed, {summary['skipped_count']} skipped")
|
|
|
|
return summary
|
|
|
|
def start_workflow(self, workflow_id: str) -> Dict[str, Any]:
|
|
"""
|
|
Start (activate) a specific workflow.
|
|
|
|
Args:
|
|
workflow_id: ID of the workflow to start
|
|
|
|
Returns:
|
|
Result of the operation
|
|
"""
|
|
try:
|
|
workflow = self.client.get_workflow(workflow_id)
|
|
if workflow.get('active', False):
|
|
return {
|
|
'success': True,
|
|
'message': f"Workflow {workflow.get('name', workflow_id)} is already active",
|
|
'was_already_active': True
|
|
}
|
|
|
|
# Activate workflow
|
|
updated_workflow = {
|
|
**workflow,
|
|
'active': True
|
|
}
|
|
result = self.client.update_workflow(workflow_id, updated_workflow)
|
|
|
|
logger.info(f"Started workflow: {workflow.get('name', workflow_id)} ({workflow_id})")
|
|
|
|
return {
|
|
'success': True,
|
|
'message': f"Successfully started workflow {workflow.get('name', workflow_id)}",
|
|
'workflow': result,
|
|
'was_already_active': False
|
|
}
|
|
|
|
except Exception as e:
|
|
error_msg = f"Failed to start workflow {workflow_id}: {e}"
|
|
logger.error(error_msg)
|
|
return {
|
|
'success': False,
|
|
'error': error_msg,
|
|
'workflow_id': workflow_id
|
|
}
|
|
|
|
def stop_workflow(self, workflow_id: str) -> Dict[str, Any]:
|
|
"""
|
|
Stop (deactivate) a specific workflow.
|
|
|
|
Args:
|
|
workflow_id: ID of the workflow to stop
|
|
|
|
Returns:
|
|
Result of the operation
|
|
"""
|
|
try:
|
|
workflow = self.client.get_workflow(workflow_id)
|
|
if not workflow.get('active', False):
|
|
return {
|
|
'success': True,
|
|
'message': f"Workflow {workflow.get('name', workflow_id)} is already inactive",
|
|
'was_already_inactive': True
|
|
}
|
|
|
|
# Store original state
|
|
self._original_states[workflow_id] = {
|
|
'active': True,
|
|
'name': workflow.get('name', 'Unknown')
|
|
}
|
|
|
|
# Deactivate workflow
|
|
updated_workflow = {
|
|
**workflow,
|
|
'active': False
|
|
}
|
|
result = self.client.update_workflow(workflow_id, updated_workflow)
|
|
|
|
logger.info(f"Stopped workflow: {workflow.get('name', workflow_id)} ({workflow_id})")
|
|
|
|
return {
|
|
'success': True,
|
|
'message': f"Successfully stopped workflow {workflow.get('name', workflow_id)}",
|
|
'workflow': result,
|
|
'was_already_inactive': False
|
|
}
|
|
|
|
except Exception as e:
|
|
error_msg = f"Failed to stop workflow {workflow_id}: {e}"
|
|
logger.error(error_msg)
|
|
return {
|
|
'success': False,
|
|
'error': error_msg,
|
|
'workflow_id': workflow_id
|
|
}
|
|
|
|
def restore_original_states(self) -> Dict[str, Any]:
|
|
"""
|
|
Restore workflows to their original states before stopping.
|
|
|
|
Returns:
|
|
Summary of restoration results
|
|
"""
|
|
if not self._original_states:
|
|
return {
|
|
'restored': [],
|
|
'failed': [],
|
|
'message': 'No original states to restore'
|
|
}
|
|
|
|
restored = []
|
|
failed = []
|
|
|
|
for workflow_id, original_state in self._original_states.items():
|
|
try:
|
|
workflow = self.client.get_workflow(workflow_id)
|
|
|
|
# Only restore if original state was active
|
|
if original_state['active']:
|
|
updated_workflow = {
|
|
**workflow,
|
|
'active': True
|
|
}
|
|
self.client.update_workflow(workflow_id, updated_workflow)
|
|
|
|
restored.append({
|
|
'id': workflow_id,
|
|
'name': original_state['name'],
|
|
'restored_to': 'active'
|
|
})
|
|
logger.info(f"Restored workflow: {original_state['name']} ({workflow_id})")
|
|
|
|
except Exception as e:
|
|
failed.append({
|
|
'id': workflow_id,
|
|
'name': original_state['name'],
|
|
'error': str(e)
|
|
})
|
|
logger.error(f"Failed to restore workflow {original_state['name']}: {e}")
|
|
|
|
# Clear stored states after restoration attempt
|
|
self._original_states.clear()
|
|
|
|
summary = {
|
|
'restored': restored,
|
|
'failed': failed,
|
|
'restored_count': len(restored),
|
|
'failed_count': len(failed)
|
|
}
|
|
|
|
logger.info(f"Restoration summary: {summary['restored_count']} restored, "
|
|
f"{summary['failed_count']} failed")
|
|
|
|
return summary
|
|
|
|
def get_workflow_states(self) -> List[Dict[str, Any]]:
|
|
"""
|
|
Get current state of all workflows.
|
|
|
|
Returns:
|
|
List of workflow states
|
|
"""
|
|
workflows = self.client.list_workflows()
|
|
states = []
|
|
|
|
for workflow in workflows:
|
|
states.append({
|
|
'id': workflow.get('id'),
|
|
'name': workflow.get('name', 'Unknown'),
|
|
'active': workflow.get('active', False),
|
|
'created_at': workflow.get('createdAt'),
|
|
'updated_at': workflow.get('updatedAt'),
|
|
'nodes_count': len(workflow.get('nodes', [])),
|
|
'connections_count': len(workflow.get('connections', {}))
|
|
})
|
|
|
|
return states
|
|
|
|
def set_workflow_inactive_with_manual_trigger(self, workflow_id: str) -> Dict[str, Any]:
|
|
"""
|
|
Set a workflow to inactive state but ensure it has a manual trigger.
|
|
This is useful for testing workflows manually.
|
|
|
|
Args:
|
|
workflow_id: ID of the workflow to modify
|
|
|
|
Returns:
|
|
Result of the operation
|
|
"""
|
|
try:
|
|
workflow = self.client.get_workflow(workflow_id)
|
|
workflow_name = workflow.get('name', 'Unknown')
|
|
|
|
# Check if workflow has a manual trigger node
|
|
nodes = workflow.get('nodes', [])
|
|
has_manual_trigger = any(
|
|
node.get('type') == 'n8n-nodes-base.manualTrigger'
|
|
for node in nodes
|
|
)
|
|
|
|
# Store original state
|
|
self._original_states[workflow_id] = {
|
|
'active': workflow.get('active', False),
|
|
'name': workflow_name
|
|
}
|
|
|
|
# Set workflow to inactive
|
|
updated_workflow = {
|
|
**workflow,
|
|
'active': False
|
|
}
|
|
|
|
# If no manual trigger exists, add one
|
|
if not has_manual_trigger:
|
|
logger.info(f"Adding manual trigger to workflow: {workflow_name}")
|
|
|
|
# Create manual trigger node
|
|
manual_trigger_node = {
|
|
"id": f"manual_trigger_{workflow_id}",
|
|
"name": "Manual Trigger",
|
|
"type": "n8n-nodes-base.manualTrigger",
|
|
"typeVersion": 1,
|
|
"position": [100, 100],
|
|
"parameters": {}
|
|
}
|
|
|
|
# Add the manual trigger node
|
|
updated_workflow['nodes'] = [manual_trigger_node] + nodes
|
|
|
|
# Update connections to include manual trigger
|
|
# This is a simplified approach - in practice, you might need more sophisticated logic
|
|
if not updated_workflow.get('connections'):
|
|
updated_workflow['connections'] = {}
|
|
|
|
# Connect manual trigger to first node if there are other nodes
|
|
if nodes:
|
|
first_node_name = nodes[0].get('name')
|
|
if first_node_name:
|
|
updated_workflow['connections']['Manual Trigger'] = {
|
|
'main': [[{'node': first_node_name, 'type': 'main', 'index': 0}]]
|
|
}
|
|
|
|
result = self.client.update_workflow(workflow_id, updated_workflow)
|
|
|
|
logger.info(f"Set workflow to inactive with manual trigger: {workflow_name} ({workflow_id})")
|
|
|
|
return {
|
|
'success': True,
|
|
'message': f"Successfully set {workflow_name} to inactive with manual trigger",
|
|
'workflow': result,
|
|
'had_manual_trigger': has_manual_trigger,
|
|
'added_manual_trigger': not has_manual_trigger
|
|
}
|
|
|
|
except Exception as e:
|
|
error_msg = f"Failed to set workflow to inactive with manual trigger {workflow_id}: {e}"
|
|
logger.error(error_msg)
|
|
return {
|
|
'success': False,
|
|
'error': error_msg,
|
|
'workflow_id': workflow_id
|
|
}
|
|
|
|
def get_active_workflows(self) -> List[Dict[str, Any]]:
|
|
"""
|
|
Get list of currently active workflows.
|
|
|
|
Returns:
|
|
List of active workflows
|
|
"""
|
|
workflows = self.client.list_workflows()
|
|
active_workflows = [
|
|
{
|
|
'id': w.get('id'),
|
|
'name': w.get('name', 'Unknown'),
|
|
'nodes_count': len(w.get('nodes', [])),
|
|
'created_at': w.get('createdAt'),
|
|
'updated_at': w.get('updatedAt')
|
|
}
|
|
for w in workflows
|
|
if w.get('active', False)
|
|
]
|
|
|
|
return active_workflows
|
|
|
|
def emergency_stop_all(self) -> Dict[str, Any]:
|
|
"""
|
|
Emergency stop of all workflows without storing states.
|
|
Use this when you need to quickly stop everything.
|
|
|
|
Returns:
|
|
Summary of emergency stop results
|
|
"""
|
|
logger.warning("Emergency stop initiated - stopping all workflows")
|
|
|
|
workflows = self.client.list_workflows()
|
|
stopped = []
|
|
failed = []
|
|
|
|
for workflow in workflows:
|
|
workflow_id = workflow.get('id')
|
|
workflow_name = workflow.get('name', 'Unknown')
|
|
|
|
if not workflow.get('active', False):
|
|
continue # Skip already inactive workflows
|
|
|
|
try:
|
|
updated_workflow = {
|
|
**workflow,
|
|
'active': False
|
|
}
|
|
self.client.update_workflow(workflow_id, updated_workflow)
|
|
|
|
stopped.append({
|
|
'id': workflow_id,
|
|
'name': workflow_name
|
|
})
|
|
|
|
except Exception as e:
|
|
failed.append({
|
|
'id': workflow_id,
|
|
'name': workflow_name,
|
|
'error': str(e)
|
|
})
|
|
|
|
summary = {
|
|
'stopped': stopped,
|
|
'failed': failed,
|
|
'stopped_count': len(stopped),
|
|
'failed_count': len(failed),
|
|
'message': f"Emergency stop completed: {len(stopped)} stopped, {len(failed)} failed"
|
|
}
|
|
|
|
logger.info(summary['message'])
|
|
return summary
|
|
|
|
def create_workflow_controller():
|
|
"""Create a workflow controller instance."""
|
|
return WorkflowController() |