Files
tkb_timeshift/claude_n8n/tools/workflow_controller.py
Docker Config Backup 8793ac4f59 Add Claude N8N toolkit with Docker mock API server
- 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>
2025-06-17 21:23:46 +02:00

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()