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